diff --git a/contracts/core/AccountFactoryV3.sol b/contracts/core/AccountFactoryV3.sol index 63c2cbed..ecc79fd5 100644 --- a/contracts/core/AccountFactoryV3.sol +++ b/contracts/core/AccountFactoryV3.sol @@ -51,8 +51,8 @@ contract AccountFactoryV3 is IAccountFactoryV3, ACLTrait, ContractsRegisterTrait /// @dev Mapping credit manager => factory params mapping(address => FactoryParams) internal _factoryParams; - /// @dev Mapping credit manager => queued accounts - mapping(address => QueuedAccount[2 ** 32]) internal _queuedAccounts; + /// @dev Mapping (credit manager, index) => queued account + mapping(address => mapping(uint256 => QueuedAccount)) internal _queuedAccounts; /// @notice Constructor /// @param addressProvider Address provider contract address diff --git a/contracts/core/BotListV3.sol b/contracts/core/BotListV3.sol index 5e8cbec1..4d252a7f 100644 --- a/contracts/core/BotListV3.sol +++ b/contracts/core/BotListV3.sol @@ -3,396 +3,207 @@ // (c) Gearbox Foundation, 2023. pragma solidity ^0.8.17; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {SafeERC20} from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; -import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import "../interfaces/IAddressProviderV3.sol"; -import {ACLNonReentrantTrait} from "../traits/ACLNonReentrantTrait.sol"; -import {IBotListV3, BotFunding, BotSpecialStatus} from "../interfaces/IBotListV3.sol"; +import {IBotListV3, BotInfo} from "../interfaces/IBotListV3.sol"; import {ICreditManagerV3} from "../interfaces/ICreditManagerV3.sol"; -import {ICreditFacadeV3} from "../interfaces/ICreditFacadeV3.sol"; +import { + AddressIsNotContractException, + CallerNotCreditFacadeException, + InvalidBotException +} from "../interfaces/IExceptions.sol"; -import "../interfaces/IExceptions.sol"; +import {ACLNonReentrantTrait} from "../traits/ACLNonReentrantTrait.sol"; +import {ContractsRegisterTrait} from "../traits/ContractsRegisterTrait.sol"; /// @title Bot list V3 -/// @notice Stores a mapping from credit accounts to bot permissions. Exists to simplify credit facades migration. -contract BotListV3 is ACLNonReentrantTrait, IBotListV3 { - using SafeCast for uint256; +/// @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 Address for address payable; using EnumerableSet for EnumerableSet.AddressSet; - using SafeERC20 for IERC20; /// @notice Contract version uint256 public constant override version = 3_00; - /// @notice Address of the DAO treasury - address public immutable override treasury; - - /// @notice Address of the WETH token - address public immutable override weth; - - /// @notice Symbol, added for ERC-20 compatibility so that bot funding could be monitored in wallets - string public constant override symbol = "gETH"; - - /// @notice Name, added for ERC-20 compatibility so that bot funding could be monitored in wallets - string public constant override name = "Gearbox bot funding"; - - /// @notice A fee in bps charged by the DAO on bot payments - uint16 public override daoFee = 0; - - /// @notice Amount of collected DAO fees in WETH - /// @dev `uint64` is chosen for tight storage packing - uint64 public override collectedDaoFees = 0; - - /// @notice Mapping from account address to its status as an approved credit manager + /// @notice Credit manager's approved status mapping(address => bool) public override approvedCreditManager; - /// @dev Set of all approved credit managers - EnumerableSet.AddressSet internal approvedCreditManagers; - - /// @notice Mapping from (creditManager, creditAccount, bot) to bot permissions - mapping(address => mapping(address => mapping(address => uint192))) public override botPermissions; - - /// @notice Mapping from (creditManager, creditAccount, bot) to bot funding parameters - mapping(address => mapping(address => mapping(address => BotFunding))) public override botFunding; - - /// @dev Mapping from credit account to the set of bots with non-zero permissions - mapping(address => mapping(address => EnumerableSet.AddressSet)) internal activeBots; - - /// @notice Mapping from (creditManager, bot) to bot's special status parameters: - /// * Whether the bot is forbidden - /// * Mask of special permissions - mapping(address => mapping(address => BotSpecialStatus)) public override botSpecialStatus; + /// @dev Mapping bot => info + mapping(address => BotInfo) internal _botInfo; - /// @notice Mapping from borrower to their bot funding balance - mapping(address => uint256) public override balanceOf; + /// @dev Mapping credit manager => credit account => set of bots with non-zero permissions + mapping(address => mapping(address => EnumerableSet.AddressSet)) internal _activeBots; - constructor(address addressProvider) ACLNonReentrantTrait(addressProvider) { - treasury = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_TREASURY, NO_VERSION_CONTROL); - weth = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_WETH_TOKEN, NO_VERSION_CONTROL); - } - - /// @dev Limits access to a function only to credit facades connected to approved credit managers + /// @dev Ensures that function can only be called by a facade connected to approved `creditManager` modifier onlyValidCreditFacade(address creditManager) { _revertIfCallerNotValidCreditFacade(creditManager); _; } - /// @notice Sets permissions and funding for (creditAccount, bot) - /// @param creditManager Credit manager to set permissions in - /// @param creditAccount Credit account to set permissions for - /// @param bot Bot to set permissions for - /// @param permissions A bit mask of permissions - /// @param totalFundingAllowance Total amount of ETH available to the bot for payments - /// @param weeklyFundingAllowance Amount of ETH available to the bot weekly - /// @return activeBotsRemaining Number of non-special bots with non-zero permissions remaining after the update - function setBotPermissions( - address creditManager, - address creditAccount, - address bot, - uint192 permissions, - uint72 totalFundingAllowance, - uint72 weeklyFundingAllowance - ) + /// @notice Constructor + /// @param addressProvider Address provider contract address + constructor(address addressProvider) + ACLNonReentrantTrait(addressProvider) + ContractsRegisterTrait(addressProvider) + {} + + // ----------- // + // PERMISSIONS // + // ----------- // + + /// @notice Returns `bot`'s permissions for `creditAccount` in `creditManager` + function botPermissions(address bot, address creditManager, address creditAccount) external + view override - nonZeroAddress(bot) - onlyValidCreditFacade(creditManager) // U:[BL-3] - returns (uint256 activeBotsRemaining) + returns (uint192) { - if (!bot.isContract()) { - revert AddressIsNotContractException(bot); // U:[BL-3] - } - - EnumerableSet.AddressSet storage accountBots = activeBots[creditManager][creditAccount]; - - if (permissions != 0) { - if ( - botSpecialStatus[creditManager][bot].forbidden - || botSpecialStatus[creditManager][bot].specialPermissions != 0 - ) { - revert InvalidBotException(); // U:[BL-3] - } - - accountBots.add(bot); // U:[BL-3] - - botPermissions[creditManager][creditAccount][bot] = permissions; // U:[BL-3] - - BotFunding storage bf = botFunding[creditManager][creditAccount][bot]; - - bf.totalFundingAllowance = totalFundingAllowance; // U:[BL-3] - bf.maxWeeklyAllowance = weeklyFundingAllowance; // U:[BL-3] - bf.remainingWeeklyAllowance = weeklyFundingAllowance; // U:[BL-3] - bf.allowanceLU = uint40(block.timestamp); // U:[BL-3] - - emit SetBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: bot, - permissions: permissions, - totalFundingAllowance: totalFundingAllowance, - weeklyFundingAllowance: weeklyFundingAllowance - }); // U:[BL-3] - } else { - _eraseBot(creditManager, creditAccount, bot); // U:[BL-3] - } - - activeBotsRemaining = accountBots.length(); // U:[BL-3] + return _botInfo[bot].permissions[creditManager][creditAccount]; } - /// @notice Removes permissions and funding for all bots with non-zero permissions for a credit account - /// @param creditManager Credit manager to erase permissions in - /// @param creditAccount Credit account to erase permissions for - function eraseAllBotPermissions(address creditManager, address creditAccount) + /// @notice Returns all bots with non-zero permissions for `creditAccount` in `creditManager` + function activeBots(address creditManager, address creditAccount) external + view override - onlyValidCreditFacade(creditManager) // U:[BL-6] + returns (address[] memory) { - EnumerableSet.AddressSet storage accountBots = activeBots[creditManager][creditAccount]; - - uint256 len = accountBots.length(); - - unchecked { - for (uint256 i = 0; i < len; ++i) { - address bot = accountBots.at(len - i - 1); // U:[BL-6] - _eraseBot({creditManager: creditManager, creditAccount: creditAccount, bot: bot}); - } - } - } - - /// @dev Removes all permissions and funding for a (creditManager, credit account, bot) tuple - function _eraseBot(address creditManager, address creditAccount, address bot) internal { - delete botPermissions[creditManager][creditAccount][bot]; // U:[BL-6] - delete botFunding[creditManager][creditAccount][bot]; // U:[BL-6] - - activeBots[creditManager][creditAccount].remove(bot); // U:[BL-6] - emit EraseBot({creditManager: creditManager, creditAccount: creditAccount, bot: bot}); // U:[BL-6] + return _activeBots[creditManager][creditAccount].values(); } - /// @notice Takes payment for performed services from the user's balance and sends to the bot - /// @dev Might transfer collected DAO fees to the treasury in case amount exceeds the limit - /// @param payer Address to charge - /// @param creditManager Address of the credit manager where the (creditAccount, bot) pair is funded - /// @param creditAccount Address of the credit account paid for - /// @param bot Address of the bot to pay - /// @param paymentAmount Amount of WETH to pay - function payBot(address payer, address creditManager, address creditAccount, address bot, uint72 paymentAmount) + /// @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) external + view override - onlyValidCreditFacade(creditManager) // U:[BL-5] + returns (uint192 permissions, bool forbidden, bool hasSpecialPermissions) { - if (paymentAmount == 0) return; - - BotFunding storage bf = botFunding[creditManager][creditAccount][bot]; // U:[BL-5] - - if (block.timestamp >= bf.allowanceLU + uint40(7 days)) { - bf.allowanceLU = uint40(block.timestamp); // U:[BL-5] - bf.remainingWeeklyAllowance = bf.maxWeeklyAllowance; // U:[BL-5] - } - - // feeAmount is always < paymentAmount, however `uint256` conversion adds more space for computations - uint72 feeAmount = uint72(uint256(daoFee) * paymentAmount / PERCENTAGE_FACTOR); // U:[BL-5] - - uint72 totalAmount = paymentAmount + feeAmount; + BotInfo storage info = _botInfo[bot]; + if (info.forbidden) return (0, true, false); - if (bf.remainingWeeklyAllowance < totalAmount) { - revert InsufficientWeeklyFundingAllowance(); - } - unchecked { - bf.remainingWeeklyAllowance -= totalAmount; // U:[BL-5] - } - - if (bf.totalFundingAllowance < totalAmount) { - revert InsufficientTotalFundingAllowance(); - } - unchecked { - bf.totalFundingAllowance -= totalAmount; // U:[BL-5] - } + uint192 specialPermissions = info.specialPermissions[creditManager]; + if (specialPermissions != 0) return (specialPermissions, false, true); - _safeDecreaseBalance(payer, totalAmount); // U:[BL-5] - - IERC20(weth).safeTransfer(bot, paymentAmount); // U:[BL-5] - - if (feeAmount != 0) { - uint256 newCollectedDaoFees = uint256(collectedDaoFees) + feeAmount; // U:[BL-5] - if (newCollectedDaoFees >= type(uint64).max) { - _transferCollectedDaoFees(newCollectedDaoFees); // U:[BL-5] - } else { - collectedDaoFees = uint64(newCollectedDaoFees); // U:[BL-5] - } - } - - emit PayBot(payer, creditAccount, bot, paymentAmount, feeAmount); // U:[BL-5] + return (info.permissions[creditManager][creditAccount], false, false); } - /// @notice Adds funds to the borrower's bot payment wallet - function deposit() public payable override nonReentrant { - if (msg.value == 0) { - revert AmountCantBeZeroException(); // U:[BL-4] - } + /// @notice Sets `bot`'s permissions for `creditAccount` in `creditManager` 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) + external + override + nonZeroAddress(bot) + onlyValidCreditFacade(creditManager) + returns (uint256 activeBotsRemaining) + { + if (!bot.isContract()) revert AddressIsNotContractException(bot); - IWETH(weth).deposit{value: msg.value}(); - balanceOf[msg.sender] += msg.value; + EnumerableSet.AddressSet storage accountBots = _activeBots[creditManager][creditAccount]; - emit Deposit(msg.sender, msg.value); // U:[BL-4] - } + if (permissions != 0) { + BotInfo storage info = _botInfo[bot]; + if (info.forbidden || info.specialPermissions[creditManager] != 0) { + revert InvalidBotException(); + } - /// @notice Removes funds from the borrower's bot payment wallet - function withdraw(uint256 amount) external override nonReentrant { - if (amount == 0) { - revert AmountCantBeZeroException(); // U:[BL-4] + accountBots.add(bot); + info.permissions[creditManager][creditAccount] = permissions; + emit SetBotPermissions(bot, creditManager, creditAccount, permissions); + } else { + _eraseBot(bot, creditManager, creditAccount); + accountBots.remove(bot); } - _safeDecreaseBalance(msg.sender, amount); - - IWETH(weth).withdraw(amount); - payable(msg.sender).sendValue(amount); // U:[BL-4] - - emit Withdraw(msg.sender, amount); // U:[BL-4] + activeBotsRemaining = accountBots.length(); } - /// @notice Returns all currently active bots on the account - function getActiveBots(address creditManager, address creditAccount) - external - view - override - returns (address[] memory) - { - return activeBots[creditManager][creditAccount].values(); - } - - /// @notice Returns information about bot permissions - function getBotStatus(address creditManager, address creditAccount, address bot) + /// @notice Removes all bots' permissions for `creditAccount` in `creditManager` + function eraseAllBotPermissions(address creditManager, address creditAccount) external - view override - returns (uint192 permissions, bool forbidden, bool hasSpecialPermissions) + onlyValidCreditFacade(creditManager) { - uint192 specialPermissions; - (forbidden, specialPermissions) = - (botSpecialStatus[creditManager][bot].forbidden, botSpecialStatus[creditManager][bot].specialPermissions); // U:[BL-7] - - hasSpecialPermissions = specialPermissions != 0; - permissions = hasSpecialPermissions ? specialPermissions : botPermissions[creditManager][creditAccount][bot]; + 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); + } + } } // ------------- // // CONFIGURATION // // ------------- // - /// @notice Sets the bot's forbidden status in a given credit manager - function setBotForbiddenStatus(address creditManager, address bot, bool status) - external - override - configuratorOnly - { - _setBotForbiddenStatus(creditManager, bot, status); + /// @notice Returns `bot`'s forbidden status + function botForbiddenStatus(address bot) external view override returns (bool) { + return _botInfo[bot].forbidden; } - /// @notice Sets the bot's forbidden status in all credit managers - function setBotForbiddenStatusEverywhere(address bot, bool status) external override configuratorOnly { - uint256 len = approvedCreditManagers.length(); - unchecked { - for (uint256 i = 0; i < len; ++i) { - _setBotForbiddenStatus(approvedCreditManagers.at(i), bot, status); - } - } + /// @notice Returns `bot`'s special permissions in `creditManager` + function botSpecialPermissions(address bot, address creditManager) external view override returns (uint192) { + return _botInfo[bot].specialPermissions[creditManager]; } - /// @dev Implementation of `setBotForbiddenStatus` - function _setBotForbiddenStatus(address creditManager, address bot, bool status) internal { - BotSpecialStatus storage bss = botSpecialStatus[creditManager][bot]; // U:[BL-7] - if (bss.forbidden != status) { - bss.forbidden = status; - emit SetBotForbiddenStatus(creditManager, bot, status); + /// @notice Sets `bot`'s status to `forbidden` + function setBotForbiddenStatus(address bot, bool forbidden) external override configuratorOnly { + BotInfo storage info = _botInfo[bot]; + if (info.forbidden != forbidden) { + info.forbidden = forbidden; + emit SetBotForbiddenStatus(bot, forbidden); } } - /// @notice Gives special permissions to a bot that extend to all credit accounts - /// @dev Bots with special permissions are DAO-approved bots which are enabled with a defined set of permissions for - /// all users. Can be used to extend system functionality with additional features without changing the core, - /// such as adding partial liquidations. - function setBotSpecialPermissions(address creditManager, address bot, uint192 permissions) + /// @notice Sets `bot`'s special permissions in `creditManager` to `permissions` + function setBotSpecialPermissions(address bot, address creditManager, uint192 permissions) external override configuratorOnly { - BotSpecialStatus storage bss = botSpecialStatus[creditManager][bot]; // U:[BL-7] - if (bss.specialPermissions != permissions) { - bss.specialPermissions = permissions; // U:[BL-7] - emit SetBotSpecialPermissions(creditManager, bot, permissions); // U:[BL-7] + BotInfo storage info = _botInfo[bot]; + if (info.specialPermissions[creditManager] != permissions) { + info.specialPermissions[creditManager] = permissions; + emit SetBotSpecialPermissions(bot, creditManager, permissions); } } - /// @notice Sets the DAO fee on bot payments - /// @param newFee The new fee value - function setDAOFee(uint16 newFee) external override configuratorOnly { - if (daoFee > PERCENTAGE_FACTOR) { - revert IncorrectParameterException(); - } - - if (daoFee != newFee) { - daoFee = newFee; // U:[BL-2] - emit SetBotDAOFee(newFee); // U:[BL-2] + /// @notice Sets `creditManager`'s status to `approved` + function setCreditManagerApprovedStatus(address creditManager, bool approved) + external + override + configuratorOnly + registeredCreditManagerOnly(creditManager) + { + if (approvedCreditManager[creditManager] != approved) { + approvedCreditManager[creditManager] = approved; + emit SetCreditManagerApprovedStatus(creditManager, approved); } } - /// @notice Sets an address' status as an approved credit manager - /// @param creditManager Address of the credit manager to change status for - /// @param newStatus The new status - function setApprovedCreditManagerStatus(address creditManager, bool newStatus) external override configuratorOnly { - if (approvedCreditManager[creditManager] != newStatus) { - if (newStatus) { - approvedCreditManagers.add(creditManager); - } else { - approvedCreditManagers.remove(creditManager); - } + // --------- // + // INTERNALS // + // --------- // - approvedCreditManager[creditManager] = newStatus; - emit SetCreditManagerStatus(creditManager, newStatus); - } - } - - /// @dev Reverts if caller is not credit facade + /// @dev Reverts if `creditManager` is not approved or caller is not a facade connected to `creditManager` function _revertIfCallerNotValidCreditFacade(address creditManager) internal view { if (!approvedCreditManager[creditManager] || ICreditManagerV3(creditManager).creditFacade() != msg.sender) { revert CallerNotCreditFacadeException(); } } - /// @notice Allows this contract to receive ETH, wraps it immediately if caller is not WETH - receive() external payable { - if (msg.sender != weth) deposit(); - } - - /// @notice Transfers all collected DAO fees to the treasury - function transferCollectedDaoFees() external override { - _transferCollectedDaoFees(collectedDaoFees); - } - - /// @dev Transfers collected DAO fees to the treasury - function _transferCollectedDaoFees(uint256 amount) internal { - if (amount > 0) { - IERC20(weth).safeTransfer(treasury, amount); // U:[BL-5] - collectedDaoFees = 0; // U:[BL-5] - } - } - - /// @dev Decreases `account`'s funding balance by `amount` - function _safeDecreaseBalance(address account, uint256 amount) internal { - if (balanceOf[account] < amount) { - revert InsufficientBalanceException(); // U:[BL-4] - } - - unchecked { - balanceOf[account] -= amount; // U:[BL-4,5] - } + /// @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); } } diff --git a/contracts/core/PriceOracleV3.sol b/contracts/core/PriceOracleV3.sol index 2d80d19b..68395f83 100644 --- a/contracts/core/PriceOracleV3.sol +++ b/contracts/core/PriceOracleV3.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.17; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import { AddressIsNotContractException, @@ -22,10 +23,13 @@ import {PriceFeedValidationTrait} from "../traits/PriceFeedValidationTrait.sol"; /// e.g., implement `latestRoundData` and always return answers with 8 decimals. /// They may also implement their own price checks, in which case they may incidcate it /// to the price oracle by returning `skipPriceCheck = true`. -/// Price oracle also allows to set a reserve price feed for a token, that can be activated +/// @notice Price oracle also allows to set a reserve price feed for a token, that can be activated /// in case the main one becomes stale or starts returning wrong values. /// One should not expect the reserve price feed to always differ from the main one, although /// most often that would be the case. +/// @notice Price oracle additionaly provides "safe" conversion functions, which use minimum of main +/// and reserve feed prices (the latter is assumed to be zero if reserve feed is not set). +/// There are also trusted price feeds, for which safe prices are simply main feed prices. contract PriceOracleV3 is ACLNonReentrantTrait, PriceFeedValidationTrait, IPriceOracleV3 { /// @notice Contract version uint256 public constant override version = 3_00; @@ -37,67 +41,98 @@ contract PriceOracleV3 is ACLNonReentrantTrait, PriceFeedValidationTrait, IPrice /// @param addressProvider Address provider contract address constructor(address addressProvider) ACLNonReentrantTrait(addressProvider) {} - /// @notice Returns `token`'s price in USD (with 8 decimals) + /// @notice Returns `token`'s price in USD (with 8 decimals) from the currently active price feed function getPrice(address token) external view override returns (uint256 price) { - (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals) = priceFeedParams(token); - (price,) = _getPrice(priceFeed, stalenessPeriod, skipCheck, decimals); + (price,) = _getPrice(token); } - /// @notice Returns `token`'s price in USD (with 8 decimals) with explicitly specified price feed + /// @notice Returns `token`'s safe price in USD (with 8 decimals) + function getPriceSafe(address token) external view override returns (uint256 price) { + (price,) = _getPriceSafe(token); + } + + /// @notice Returns `token`'s price in USD (with 8 decimals) from the specified price feed function getPriceRaw(address token, bool reserve) external view returns (uint256 price) { - (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals,) = - _getPriceFeedParams(reserve ? _getTokenReserveKey(token) : token); - if (priceFeed == address(0)) revert PriceFeedDoesNotExistException(); - (price,) = _getPrice(priceFeed, stalenessPeriod, skipCheck, decimals); + (price,) = _getPriceRaw(token, reserve); } /// @notice Converts `amount` of `token` into USD amount (with 8 decimals) - function convertToUSD(uint256 amount, address token) public view override returns (uint256) { - (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals) = priceFeedParams(token); - (uint256 price, uint256 scale) = _getPrice(priceFeed, stalenessPeriod, skipCheck, decimals); + function convertToUSD(uint256 amount, address token) external view override returns (uint256) { + (uint256 price, uint256 scale) = _getPrice(token); return amount * price / scale; // U:[PO-9] } /// @notice Converts `amount` of USD (with 8 decimals) into `token` amount - function convertFromUSD(uint256 amount, address token) public view override returns (uint256) { - (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals) = priceFeedParams(token); - (uint256 price, uint256 scale) = _getPrice(priceFeed, stalenessPeriod, skipCheck, decimals); + function convertFromUSD(uint256 amount, address token) external view override returns (uint256) { + (uint256 price, uint256 scale) = _getPrice(token); return amount * scale / price; // U:[PO-9] } /// @notice Converts `amount` of `tokenFrom` into `tokenTo` amount function convert(uint256 amount, address tokenFrom, address tokenTo) external view override returns (uint256) { - (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals) = priceFeedParams(tokenFrom); - (uint256 priceFrom, uint256 scaleFrom) = _getPrice(priceFeed, stalenessPeriod, skipCheck, decimals); - - (priceFeed, stalenessPeriod, skipCheck, decimals) = priceFeedParams(tokenTo); - (uint256 priceTo, uint256 scaleTo) = _getPrice(priceFeed, stalenessPeriod, skipCheck, decimals); - + (uint256 priceFrom, uint256 scaleFrom) = _getPrice(tokenFrom); + (uint256 priceTo, uint256 scaleTo) = _getPrice(tokenTo); return amount * priceFrom * scaleTo / (priceTo * scaleFrom); // U:[PO-10] } + /// @notice Converts `amount` of `token` into USD amount (with 8 decimals) using safe price + function safeConvertToUSD(uint256 amount, address token) external view override returns (uint256) { + (uint256 price, uint256 scale) = _getPriceSafe(token); + return amount * price / scale; // U:[PO-11] + } + /// @notice Returns the price feed for `token` or reverts if price feed is not set function priceFeeds(address token) external view override returns (address priceFeed) { - (priceFeed,,,) = priceFeedParams(token); // U:[PO-8] + (priceFeed,,,,) = priceFeedParams(token); // U:[PO-8] } - /// @notice Returns the price feed for `token` with explicitly specified price feed + /// @notice Returns the specified price feed for `token` function priceFeedsRaw(address token, bool reserve) external view override returns (address priceFeed) { - (priceFeed,,,,) = _getPriceFeedParams(reserve ? _getTokenReserveKey(token) : token); + (priceFeed,,,,,) = _getPriceFeedParams(reserve ? _getTokenReserveKey(token) : token); } - /// @notice Returns price feed parameters for `token` or reverts if price feed is not set + /// @notice Returns currently active price feed parameters for `token` or reverts if price feed is not set function priceFeedParams(address token) public view override - returns (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals) + returns (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals, bool trusted) { bool useReserve; - (priceFeed, stalenessPeriod, skipCheck, decimals, useReserve) = _getPriceFeedParams(token); - if (decimals == 0) revert PriceFeedDoesNotExistException(); + (priceFeed, stalenessPeriod, skipCheck, decimals, useReserve, trusted) = _getPriceFeedParams(token); if (useReserve) { - (priceFeed, stalenessPeriod, skipCheck, decimals,) = _getPriceFeedParams(_getTokenReserveKey(token)); + (priceFeed, stalenessPeriod, skipCheck, decimals,, trusted) = + _getPriceFeedParams(_getTokenReserveKey(token)); + } + } + + // --------- // + // INTERNALS // + // --------- // + + /// @dev Returns `token`'s price and scale from the currently active price feed + function _getPrice(address token) internal view returns (uint256 price, uint256 scale) { + (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals,) = priceFeedParams(token); + return _getPrice(priceFeed, stalenessPeriod, skipCheck, decimals); + } + + /// @dev Returns `token`'s price and scale from the explicitly specified price feed + function _getPriceRaw(address token, bool reserve) internal view returns (uint256 price, uint256 scale) { + (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals,,) = + _getPriceFeedParams(reserve ? _getTokenReserveKey(token) : token); + return _getPrice(priceFeed, stalenessPeriod, skipCheck, decimals); + } + + /// @dev Returns `token`'s safe price and scale + function _getPriceSafe(address token) internal view returns (uint256 price, uint256 scale) { + (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals,, bool trusted) = + _getPriceFeedParams(token); + (price, scale) = _getPrice(priceFeed, stalenessPeriod, skipCheck, decimals); // U:[PO-11] + + if (!trusted) { + if (_priceFeedsParams[_getTokenReserveKey(token)].priceFeed == address(0)) return (0, scale); // U:[PO-11] + (uint256 reservePrice,) = _getPriceRaw(token, true); + price = Math.min(price, reservePrice); // U:[PO-11] } } @@ -122,7 +157,14 @@ contract PriceOracleV3 is ACLNonReentrantTrait, PriceFeedValidationTrait, IPrice function _getPriceFeedParams(address token) internal view - returns (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals, bool useReserve) + returns ( + address priceFeed, + uint32 stalenessPeriod, + bool skipCheck, + uint8 decimals, + bool useReserve, + bool trusted + ) { PriceFeedParams storage params = _priceFeedsParams[token]; assembly { @@ -132,7 +174,10 @@ contract PriceOracleV3 is ACLNonReentrantTrait, PriceFeedValidationTrait, IPrice skipCheck := and(shr(192, data), 0x01) decimals := shr(200, data) useReserve := and(shr(208, data), 0x01) + trusted := and(shr(216, data), 0x01) } // U:[PO-2] + + if (priceFeed == address(0)) revert PriceFeedDoesNotExistException(); // U:[PO-2] } /// @dev Returns key that is used to store `token`'s reserve feed in `_priceFeedParams` @@ -149,7 +194,7 @@ contract PriceOracleV3 is ACLNonReentrantTrait, PriceFeedValidationTrait, IPrice // ------------- // /// @notice Sets price feed for a given token - function setPriceFeed(address token, address priceFeed, uint32 stalenessPeriod) + function setPriceFeed(address token, address priceFeed, uint32 stalenessPeriod, bool trusted) external override nonZeroAddress(token) // U:[PO-6] @@ -165,9 +210,10 @@ contract PriceOracleV3 is ACLNonReentrantTrait, PriceFeedValidationTrait, IPrice stalenessPeriod: stalenessPeriod, skipCheck: skipCheck, decimals: decimals, - useReserve: false + useReserve: false, + trusted: trusted }); // U:[PO-6] - emit SetPriceFeed(token, priceFeed, stalenessPeriod, skipCheck); // U:[PO-6] + emit SetPriceFeed(token, priceFeed, stalenessPeriod, skipCheck, trusted); // U:[PO-6] } /// @notice Sets reserve price feed for a given token @@ -188,7 +234,8 @@ contract PriceOracleV3 is ACLNonReentrantTrait, PriceFeedValidationTrait, IPrice stalenessPeriod: stalenessPeriod, skipCheck: skipCheck, decimals: decimals, - useReserve: false + useReserve: false, + trusted: false }); // U:[PO-7] emit SetReservePriceFeed(token, priceFeed, stalenessPeriod, skipCheck); // U:[PO-7] } diff --git a/contracts/core/WithdrawalManagerV3.sol b/contracts/core/WithdrawalManagerV3.sol deleted file mode 100644 index e91a426b..00000000 --- a/contracts/core/WithdrawalManagerV3.sol +++ /dev/null @@ -1,316 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Foundation, 2023. -pragma solidity ^0.8.17; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - -import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; - -import {AP_WETH_TOKEN, IAddressProviderV3, NO_VERSION_CONTROL} from "../interfaces/IAddressProviderV3.sol"; -import { - AmountCantBeZeroException, - CallerNotCreditManagerException, - NoFreeWithdrawalSlotsException, - NothingToClaimException, - ReceiveIsNotAllowedException -} from "../interfaces/IExceptions.sol"; -import { - ClaimAction, ETH_ADDRESS, IWithdrawalManagerV3, ScheduledWithdrawal -} from "../interfaces/IWithdrawalManagerV3.sol"; -import {UnsafeERC20} from "../libraries/UnsafeERC20.sol"; -import {WithdrawalsLogic} from "../libraries/WithdrawalsLogic.sol"; -import {ACLTrait} from "../traits/ACLTrait.sol"; -import {ContractsRegisterTrait} from "../traits/ContractsRegisterTrait.sol"; - -/// @title Withdrawal manager -/// @notice Contract that handles withdrawals from credit accounts. -/// There are two kinds of withdrawals: immediate and scheduled. -/// - Immediate withdrawals can be claimed, well, immediately, and exist to support blacklistable tokens -/// and WETH unwrapping upon credit account closure/liquidation. -/// - Scheduled withdrawals can be claimed after a certain delay, and exist to support partial withdrawals -/// from credit accounts. One credit account can have up to two scheduled withdrawals at the same time. -contract WithdrawalManagerV3 is IWithdrawalManagerV3, ACLTrait, ContractsRegisterTrait { - using SafeERC20 for IERC20; - using UnsafeERC20 for IERC20; - using Address for address payable; - using EnumerableSet for EnumerableSet.AddressSet; - using WithdrawalsLogic for ClaimAction; - using WithdrawalsLogic for ScheduledWithdrawal; - using WithdrawalsLogic for ScheduledWithdrawal[2]; - - /// @notice Contract version - uint256 public constant override version = 3_00; - - /// @notice WETH token address - address public immutable override weth; - - /// @notice Mapping account => token => claimable amount - mapping(address => mapping(address => uint256)) public override immediateWithdrawals; - - /// @notice Delay for scheduled withdrawals - uint40 public override delay; - - /// @dev Mapping credit account => scheduled withdrawals - mapping(address => ScheduledWithdrawal[2]) internal _scheduled; - - /// @notice Mapping from address to its status as an approved credit manager - mapping(address => bool) public override isValidCreditManager; - - /// @dev Ensures that function caller is one of added credit managers - modifier creditManagerOnly() { - _ensureCallerIsCreditManager(); - _; - } - - /// @notice Constructor - /// @param _addressProvider Address of the address provider - /// @param _delay Delay for scheduled withdrawals - constructor(address _addressProvider, uint40 _delay) - ACLTrait(_addressProvider) - ContractsRegisterTrait(_addressProvider) - { - weth = IAddressProviderV3(_addressProvider).getAddressOrRevert(AP_WETH_TOKEN, NO_VERSION_CONTROL); // U:[WM-1] - - if (_delay != 0) { - delay = _delay; // U:[WM-1] - } - emit SetWithdrawalDelay(_delay); - } - - /// @notice Allows this contract to unwrap WETH and forbids receiving ETH another way - receive() external payable { - if (msg.sender != weth) revert ReceiveIsNotAllowedException(); // U:[WM-2] - } - - // --------------------- // - // IMMEDIATE WITHDRAWALS // - // --------------------- // - - /// @notice Adds new immediate withdrawal for the account - /// @param token Token to withdraw - /// @param to Account to add immediate withdrawal for - /// @param amount Amount to withdraw - /// @custom:expects Credit manager transferred `amount` of `token` to this contract prior to calling this function - function addImmediateWithdrawal(address token, address to, uint256 amount) - external - override - creditManagerOnly // U:[WM-2] - { - _addImmediateWithdrawal({account: to, token: token, amount: amount}); // U:[WM-3] - } - - /// @notice Claims caller's immediate withdrawal - /// @param token Token to claim (if `ETH_ADDRESS`, claims WETH, but unwraps it before sending) - /// @param to Token recipient - function claimImmediateWithdrawal(address token, address to) - external - override - nonZeroAddress(to) // U:[WM-4A] - { - bool isETH = token == ETH_ADDRESS; - - _claimImmediateWithdrawal({account: msg.sender, token: isETH ? weth : token, to: to, unwrapWETH: isETH}); // U:[WM-4B,4C,4D] - } - - /// @dev Increases `account`'s immediately withdrawable balance of `token` by `amount` - function _addImmediateWithdrawal(address account, address token, uint256 amount) internal { - if (amount > 1) { - immediateWithdrawals[account][token] += amount; // U:[WM-3] - emit AddImmediateWithdrawal(account, token, amount); // U:[WM-3] - } - } - - /// @dev Sends all `account`'s immediately withdrawable balance of `token` to `to` - function _claimImmediateWithdrawal(address account, address token, address to, bool unwrapWETH) internal { - uint256 amount = immediateWithdrawals[account][token]; - if (amount <= 1) return; - unchecked { - --amount; // U:[WM-4C,4D] - } - immediateWithdrawals[account][token] = 1; // U:[WM-4C,4D] - _safeTransfer(token, to, amount, unwrapWETH); // U:[WM-4C,4D] - emit ClaimImmediateWithdrawal(account, token, to, amount); // U:[WM-4C,4D] - } - - /// @dev Transfers token, optionally unwraps WETH before sending - function _safeTransfer(address token, address to, uint256 amount, bool unwrapWETH) internal { - if (unwrapWETH && token == weth) { - IWETH(weth).withdraw(amount); // U:[WM-4D] - payable(to).sendValue(amount); // U:[WM-4D] - } else { - IERC20(token).safeTransfer(to, amount); // U:[WM-4C] - } - } - - // --------------------- // - // SCHEDULED WITHDRAWALS // - // --------------------- // - - /// @notice Returns withdrawals scheduled for a given credit account - /// @param creditAccount Account to get withdrawals for - /// @return withdrawals See `ScheduledWithdrawal` - function scheduledWithdrawals(address creditAccount) - external - view - override - returns (ScheduledWithdrawal[2] memory) - { - return _scheduled[creditAccount]; - } - - /// @notice Schedules withdrawal from the credit account - /// @param creditAccount Account to withdraw from - /// @param token Token to withdraw - /// @param amount Amount to withdraw - /// @param tokenIndex Collateral index of withdrawn token in account's credit manager - /// @custom:expects Credit manager transferred `amount` of `token` to this contract prior to calling this function - function addScheduledWithdrawal(address creditAccount, address token, uint256 amount, uint8 tokenIndex) - external - override - creditManagerOnly // U:[WM-2] - { - if (amount <= 1) { - revert AmountCantBeZeroException(); // U:[WM-5A] - } - ScheduledWithdrawal[2] storage withdrawals = _scheduled[creditAccount]; - (bool found, uint8 slot) = withdrawals.findFreeSlot(); // U:[WM-5B] - if (!found) revert NoFreeWithdrawalSlotsException(); // U:[WM-5B] - - uint40 maturity = uint40(block.timestamp) + delay; - withdrawals[slot] = - ScheduledWithdrawal({tokenIndex: tokenIndex, token: token, maturity: maturity, amount: amount}); // U:[WM-5B] - emit AddScheduledWithdrawal(creditAccount, token, amount, maturity); // U:[WM-5B] - } - - /// @notice Claims scheduled withdrawals from the credit account - /// - Withdrawals are either sent to `to` or returned to `creditAccount` based on maturity and `action` - /// - If `to` is blacklisted in claimed token, scheduled withdrawal turns into immediate - /// @param creditAccount Account withdrawal was made from - /// @param to Address to send withdrawals to - /// @param action See `ClaimAction` - /// @return hasScheduled Whether account has at least one scheduled withdrawal after claiming - /// @return tokensToEnable Bit mask of returned tokens that should be enabled as account's collateral - function claimScheduledWithdrawals(address creditAccount, address to, ClaimAction action) - external - override - creditManagerOnly // U:[WM-2] - returns (bool hasScheduled, uint256 tokensToEnable) - { - ScheduledWithdrawal[2] storage withdrawals = _scheduled[creditAccount]; - - (bool scheduled0, bool claimed0, uint256 tokensToEnable0) = - _processScheduledWithdrawal(withdrawals[0], action, creditAccount, to); // U:[WM-6B] - (bool scheduled1, bool claimed1, uint256 tokensToEnable1) = - _processScheduledWithdrawal(withdrawals[1], action, creditAccount, to); // U:[WM-6B] - - if (action == ClaimAction.CLAIM && !(claimed0 || claimed1)) { - revert NothingToClaimException(); // U:[WM-6A] - } - - hasScheduled = scheduled0 || scheduled1; // U:[WM-6B] - tokensToEnable = tokensToEnable0 | tokensToEnable1; // U:[WM-6B] - } - - /// @notice Returns scheduled withdrawals from the credit account that can be cancelled - function cancellableScheduledWithdrawals(address creditAccount, bool isForceCancel) - external - view - override - returns (address token1, uint256 amount1, address token2, uint256 amount2) - { - ScheduledWithdrawal[2] storage withdrawals = _scheduled[creditAccount]; - ClaimAction action = isForceCancel ? ClaimAction.FORCE_CANCEL : ClaimAction.CANCEL; // U:[WM-7] - if (action.cancelAllowed(withdrawals[0].maturity)) { - (token1,, amount1) = withdrawals[0].tokenMaskAndAmount(); // U:[WM-7] - } - if (action.cancelAllowed(withdrawals[1].maturity)) { - (token2,, amount2) = withdrawals[1].tokenMaskAndAmount(); // U:[WM-7] - } - } - - /// @dev Claims or cancels withdrawal based on its maturity and action type - function _processScheduledWithdrawal( - ScheduledWithdrawal storage w, - ClaimAction action, - address creditAccount, - address to - ) internal returns (bool scheduled, bool claimed, uint256 tokensToEnable) { - uint40 maturity = w.maturity; - scheduled = maturity > 1; // U:[WM-8] - if (action.claimAllowed(maturity)) { - _claimScheduledWithdrawal(w, creditAccount, to); // U:[WM-8] - scheduled = false; // U:[WM-8] - claimed = true; // U:[WM-8] - } else if (action.cancelAllowed(maturity)) { - tokensToEnable = _cancelScheduledWithdrawal(w, creditAccount); // U:[WM-8] - scheduled = false; // U:[WM-8] - } - } - - /// @dev Claims scheduled withdrawal, clears withdrawal in storage - /// @custom:expects Withdrawal is scheduled - function _claimScheduledWithdrawal(ScheduledWithdrawal storage w, address creditAccount, address to) internal { - (address token,, uint256 amount) = w.tokenMaskAndAmount(); // U:[WM-9A,9B] - w.clear(); // U:[WM-9A,9B] - emit ClaimScheduledWithdrawal(creditAccount, token, to, amount); // U:[WM-9A,9B] - - bool success = IERC20(token).unsafeTransfer(to, amount); // U:[WM-9A] - if (!success) _addImmediateWithdrawal(to, token, amount); // U:[WM-9B] - } - - /// @dev Cancels withdrawal, clears withdrawal in storage - /// @custom:expects Withdrawal is scheduled - function _cancelScheduledWithdrawal(ScheduledWithdrawal storage w, address creditAccount) - internal - returns (uint256 tokensToEnable) - { - (address token, uint256 tokenMask, uint256 amount) = w.tokenMaskAndAmount(); // U:[WM-10] - w.clear(); // U:[WM-10] - emit CancelScheduledWithdrawal(creditAccount, token, amount); // U:[WM-10] - - IERC20(token).safeTransfer(creditAccount, amount); // U:[WM-10] - tokensToEnable = tokenMask; // U:[WM-10] - } - - // ------------- // - // CONFIGURATION // - // ------------- // - - /// @notice Sets delay for scheduled withdrawals, only affects new withdrawal requests - /// @param newDelay New delay for scheduled withdrawals - function setWithdrawalDelay(uint40 newDelay) - external - override - configuratorOnly // U:[WM-2] - { - if (newDelay != delay) { - delay = newDelay; // U:[WM-11] - emit SetWithdrawalDelay(newDelay); // U:[WM-11] - } - } - - /// @notice Adds new credit manager that can interact with this contract - /// @param newCreditManager New credit manager to add - function addCreditManager(address newCreditManager) - external - override - configuratorOnly // U:[WM-2] - registeredCreditManagerOnly(newCreditManager) // U:[WM-12A] - { - if (!isValidCreditManager[newCreditManager]) { - isValidCreditManager[newCreditManager] = true; // U:[WM-12B] - emit AddCreditManager(newCreditManager); // U:[WM-12B] - } - } - - /// @dev Ensures caller is one of added credit managers - function _ensureCallerIsCreditManager() internal view { - if (!isValidCreditManager[msg.sender]) { - revert CallerNotCreditManagerException(); - } - } -} diff --git a/contracts/credit/CreditConfiguratorV3.sol b/contracts/credit/CreditConfiguratorV3.sol index 8ab73fc8..78f71950 100644 --- a/contracts/credit/CreditConfiguratorV3.sol +++ b/contracts/credit/CreditConfiguratorV3.sol @@ -659,7 +659,7 @@ contract CreditConfiguratorV3 is ICreditConfiguratorV3, ACLNonReentrantTrait { uint256 mask = forbiddenTokensMask & uint256(-int256(forbiddenTokensMask)); address token = CreditManagerV3(creditManager).getTokenByMask(mask); _forbidToken(_creditFacade, token); - forbiddenTokensMask &= forbiddenTokensMask - 1; + forbiddenTokensMask ^= mask; } } } diff --git a/contracts/credit/CreditFacadeV3.sol b/contracts/credit/CreditFacadeV3.sol index 37d245ce..36f209c1 100644 --- a/contracts/credit/CreditFacadeV3.sol +++ b/contracts/credit/CreditFacadeV3.sol @@ -12,7 +12,7 @@ import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC2 import {SafeERC20} from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol"; // LIBS & TRAITS -import {BalancesLogic, Balance, BalanceDelta, BalanceWithMask} from "../libraries/BalancesLogic.sol"; +import {BalancesLogic, Balance, BalanceDelta, BalanceWithMask, Comparison} from "../libraries/BalancesLogic.sol"; import {ACLNonReentrantTrait} from "../traits/ACLNonReentrantTrait.sol"; import {BitMask, UNDERLYING_TOKEN_MASK} from "../libraries/BitMask.sol"; @@ -21,7 +21,6 @@ import "../interfaces/ICreditFacadeV3.sol"; import "../interfaces/IAddressProviderV3.sol"; import { ICreditManagerV3, - ClosureAction, ManageDebtAction, RevocationPair, CollateralDebtData, @@ -30,8 +29,7 @@ import { INACTIVE_CREDIT_ACCOUNT_ADDRESS } from "../interfaces/ICreditManagerV3.sol"; import {AllowanceAction} from "../interfaces/ICreditConfiguratorV3.sol"; -import {ClaimAction, ETH_ADDRESS, IWithdrawalManagerV3} from "../interfaces/IWithdrawalManagerV3.sol"; -import {IPriceOracleBase} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracleBase.sol"; +import {IPriceOracleV3} from "../interfaces/IPriceOracleV3.sol"; import {IUpdatablePriceFeed} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceFeed.sol"; import {IPoolV3} from "../interfaces/IPoolV3.sol"; @@ -45,25 +43,31 @@ import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/C // EXCEPTIONS import "../interfaces/IExceptions.sol"; -uint256 constant OPEN_CREDIT_ACCOUNT_FLAGS = ALL_PERMISSIONS & ~WITHDRAW_PERMISSION; +uint256 constant OPEN_CREDIT_ACCOUNT_FLAGS = + ALL_PERMISSIONS & ~(DECREASE_DEBT_PERMISSION | WITHDRAW_COLLATERAL_PERMISSION); -uint256 constant CLOSE_CREDIT_ACCOUNT_FLAGS = EXTERNAL_CALLS_PERMISSION; +uint256 constant CLOSE_CREDIT_ACCOUNT_FLAGS = ALL_PERMISSIONS & ~INCREASE_DEBT_PERMISSION; + +uint256 constant LIQUIDATE_CREDIT_ACCOUNT_FLAGS = + EXTERNAL_CALLS_PERMISSION | ADD_COLLATERAL_PERMISSION | WITHDRAW_COLLATERAL_PERMISSION; /// @title Credit facade V3 /// @notice Provides a user interface to open, close and liquidate leveraged positions in the credit manager, /// and implements the main entry-point for credit accounts management: multicall. -/// @notice Multicall allows account owners to batch all the desired operations (changing debt size, interacting with -/// external protocols via adapters, increasing quotas or scheduling withdrawals) into one call, followed by -/// the collateral check that ensures that account is sufficiently collateralized. +/// @notice Multicall allows account owners to batch all the desired operations (adding or withdrawing collateral, +/// changing debt size, interacting with external protocols via adapters or increasing quotas) into one call, +/// followed by the collateral check that ensures that account is sufficiently collateralized. /// For more details on what one can achieve with multicalls, see `_multicall` and `ICreditFacadeV3Multicall`. /// @notice Users can also let external bots manage their accounts via `botMulticall`. Bots can be relatively general, /// the facade only ensures that they can do no harm to the protocol by running the collateral check after the /// multicall and checking the permissions given to them by users. See `BotListV3` for additional details. /// @notice Credit facade implements a few safeguards on top of those present in the credit manager, including debt and /// quota size validation, pausing on large protocol losses, Degen NFT whitelist mode, and forbidden tokens -/// (they count towards account value, but having them enabled as collateral restricts available actions). +/// (they count towards account value, but having them enabled as collateral restricts available actions and +/// activates a safer version of collateral check). contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { using Address for address; + using Address for address payable; using BitMask for uint256; using SafeCast for uint256; using SafeERC20 for IERC20; @@ -86,9 +90,6 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { /// @notice WETH token address address public immutable override weth; - /// @notice Withdrawal manager address - address public immutable override withdrawalManager; - /// @notice Degen NFT address address public immutable override degenNFT; @@ -159,7 +160,6 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { creditManager = _creditManager; // U:[FA-1] weth = ICreditManagerV3(_creditManager).weth(); // U:[FA-1] - withdrawalManager = ICreditManagerV3(_creditManager).withdrawalManager(); // U:[FA-1] botList = IAddressProviderV3(ICreditManagerV3(_creditManager).addressProvider()).getAddressOrRevert(AP_BOT_LIST, 3_00); @@ -176,7 +176,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { /// - Wraps any ETH sent in the function call and sends it back to the caller /// - If Degen NFT is enabled, burns one from the caller /// - Opens an account in the credit manager - /// - Performs a multicall (all calls allowed except withdrawals) + /// - Performs a multicall (all calls allowed except debt decrease and withdrawals) /// - Runs the collateral check /// @param onBehalfOf Address on whose behalf to open the account /// @param calls List of calls to perform after opening the account @@ -205,48 +205,41 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { emit OpenCreditAccount(creditAccount, onBehalfOf, msg.sender, referralCode); // U:[FA-10] - // same as `_multicallFullCollateralCheck` but leverages the fact that account is freshly opened to save gas - BalanceWithMask[] memory forbiddenBalances; + if (calls.length != 0) { + // same as `_multicallFullCollateralCheck` but leverages the fact that account is freshly opened to save gas + BalanceWithMask[] memory forbiddenBalances; - uint256 skipCalls = _applyOnDemandPriceUpdates(calls); - FullCheckParams memory fullCheckParams = _multicall({ - creditAccount: creditAccount, - calls: calls, - enabledTokensMask: 0, - flags: OPEN_CREDIT_ACCOUNT_FLAGS, - skip: skipCalls - }); // U:[FA-10] + uint256 skipCalls = _applyOnDemandPriceUpdates(calls); + FullCheckParams memory fullCheckParams = _multicall({ + creditAccount: creditAccount, + calls: calls, + enabledTokensMask: 0, + flags: OPEN_CREDIT_ACCOUNT_FLAGS, + skip: skipCalls + }); // U:[FA-10] - _fullCollateralCheck({ - creditAccount: creditAccount, - enabledTokensMaskBefore: 0, - fullCheckParams: fullCheckParams, - forbiddenBalances: forbiddenBalances, - _forbiddenTokenMask: forbiddenTokenMask - }); // U:[FA-10] + _fullCollateralCheck({ + creditAccount: creditAccount, + enabledTokensMaskBefore: 0, + fullCheckParams: fullCheckParams, + forbiddenBalances: forbiddenBalances, + forbiddenTokensMask: forbiddenTokenMask + }); // U:[FA-10] + } } /// @notice Closes a credit account /// - Wraps any ETH sent in the function call and sends it back to the caller - /// - Claims all scheduled withdrawals + /// - Performs a multicall (all calls are allowed except debt increase) + /// - Closes a credit account in the credit manager /// - Erases all bots permissions - /// - Performs a multicall (only adapter calls allowed) - /// - Closes a credit account in the credit manager (all debt must be repaid for this step to succeed) /// @param creditAccount Account to close - /// @param to Address to send withdrawals and any tokens left on the account after closure - /// @param skipTokenMask Bit mask of tokens that should be skipped - /// @param convertToETH Whether to unwrap WETH before sending to `to` /// @param calls List of calls to perform before closing the account /// @dev Reverts if caller is not `creditAccount`'s owner /// @dev Reverts if facade is paused - /// @dev Reverts if account's debt was updated in the same block - function closeCreditAccount( - address creditAccount, - address to, - uint256 skipTokenMask, - bool convertToETH, - MultiCall[] calldata calls - ) + /// @dev Reverts if account has enabled tokens after executing `calls` + /// @dev Reverts if account's debt is not zero after executing `calls` + function closeCreditAccount(address creditAccount, MultiCall[] calldata calls) external payable override @@ -255,128 +248,105 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { nonReentrant // U:[FA-4] wrapETH // U:[FA-7] { - CollateralDebtData memory debtData = _calcDebtAndCollateral(creditAccount, CollateralCalcTask.DEBT_ONLY); // U:[FA-11] - - _claimWithdrawals(creditAccount, to, ClaimAction.FORCE_CLAIM); // U:[FA-11] + uint256 enabledTokensMask = _enabledTokensMaskOf(creditAccount); if (calls.length != 0) { - uint256 skipCalls = _applyOnDemandPriceUpdates(calls); - FullCheckParams memory fullCheckParams = - _multicall(creditAccount, calls, debtData.enabledTokensMask, CLOSE_CREDIT_ACCOUNT_FLAGS, skipCalls); // U:[FA-11] - debtData.enabledTokensMask = fullCheckParams.enabledTokensMaskAfter; // U:[FA-11] + _multicall(creditAccount, calls, enabledTokensMask, CLOSE_CREDIT_ACCOUNT_FLAGS, 0); // U:[FA-11] + enabledTokensMask = fullCheckParams.enabledTokensMaskAfter; } - _eraseAllBotPermissions({creditAccount: creditAccount}); // U:[FA-11] - - _closeCreditAccount({ - creditAccount: creditAccount, - closureAction: ClosureAction.CLOSE_ACCOUNT, - collateralDebtData: debtData, - payer: msg.sender, - to: to, - skipTokensMask: skipTokenMask, - convertToETH: convertToETH - }); // U:[FA-11] + if (enabledTokensMask != 0) revert CloseAccountWithEnabledTokensException(); // U:[FA-11] - if (convertToETH) { - _wethWithdrawTo(to); // U:[FA-11] + if (_flagsOf(creditAccount) & BOT_PERMISSIONS_SET_FLAG != 0) { + IBotListV3(botList).eraseAllBotPermissions(creditManager, creditAccount); // U:[FA-11] } - emit CloseCreditAccount(creditAccount, msg.sender, to); // U:[FA-11] + ICreditManagerV3(creditManager).closeCreditAccount(creditAccount); // U:[FA-11] + + emit CloseCreditAccount(creditAccount, msg.sender); // U:[FA-11] } /// @notice Liquidates a credit account /// - Updates price feeds before running all computations if such calls are present in the multicall /// - Evaluates account's collateral and debt to determine whether liquidated account is unhealthy or expired - /// - Cancels immature scheduled withdrawals and returns tokens to the account (on emergency, even mature - /// withdrawals are returned) - /// - Performs a multicall (only adapter calls allowed) - /// - Erases all bots permissions - /// - Closes a credit account in the credit manager, distributing the funds between pool, owner and liquidator + /// - Performs a multicall (only `addCollateral`, `withdrawCollateral` and adapter calls are allowed) + /// - Liquidates a credit account in the credit manager, which repays debt to the pool, removes quotas, and + /// transfers underlying to the liquidator /// - If pool incurs a loss on liquidation, further borrowing through the facade is forbidden /// - If cumulative loss from bad debt liquidations exceeds the threshold, the facade is paused - /// @notice Typically, a liquidator would swap all holdings on the account to underlying via multicall and receive - /// the premium. An alternative strategy would be to allow credit manager to take underlying shortfall from - /// the caller and receive all account's holdings directly to handle them in another way. + /// @notice The function computes account’s total value (oracle value of enabled tokens), discounts it by liquidator’s + /// premium, and uses this value to compute funds due to the pool and owner. + /// Debt to the pool must be repaid in underlying, while funds due to owner might be covered by underlying + /// as well as by tokens that counted towards total value calculation, with the only condition that balance + /// of such tokens can’t be increased in the multicall. + /// Typically, a liquidator would swap all holdings on the account to underlying via multicall and receive + /// the premium in underlying. + /// An alternative strategy would be to add underlying collateral to repay debt and withdraw desired tokens + /// to handle them in another way, while remaining tokens would cover funds due to owner. /// @param creditAccount Account to liquidate - /// @param to Address to send tokens left on the account after closure and funds distribution - /// @param skipTokenMask Bit mask of tokens that should be skipped - /// @param convertToETH Whether to unwrap WETH before sending to `to` + /// @param to Address to transfer underlying left after liquidation /// @param calls List of calls to perform before liquidating the account /// @dev When the credit facade is paused, reverts if caller is not an approved emergency liquidator /// @dev Reverts if `creditAccount` is not opened in connected credit manager /// @dev Reverts if account is not liquidatable - /// @dev Reverts if account's debt was updated in the same block - function liquidateCreditAccount( - address creditAccount, - address to, - uint256 skipTokenMask, - bool convertToETH, - MultiCall[] calldata calls - ) + /// @dev Reverts if remaining token balances increase during the multicall + function liquidateCreditAccount(address creditAccount, address to, MultiCall[] calldata calls) external override whenNotPausedOrEmergency // U:[FA-2,12] nonReentrant // U:[FA-4] { - // saves gas for late liquidations address borrower = _getBorrowerOrRevert(creditAccount); // U:[FA-5] uint256 skipCalls = _applyOnDemandPriceUpdates(calls); - ClosureAction closeAction; - CollateralDebtData memory collateralDebtData; - { - bool isEmergency = paused(); - - collateralDebtData = _calcDebtAndCollateral( - creditAccount, - isEmergency - ? CollateralCalcTask.DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS - : CollateralCalcTask.DEBT_COLLATERAL_CANCEL_WITHDRAWALS - ); // U:[FA-15] - - closeAction = ClosureAction.LIQUIDATE_ACCOUNT; // U:[FA-14] + CollateralDebtData memory collateralDebtData = + ICreditManagerV3(creditManager).calcDebtAndCollateral(creditAccount, CollateralCalcTask.DEBT_COLLATERAL); // U:[FA-15] + bool isExpired; + { bool isLiquidatable = collateralDebtData.twvUSD < collateralDebtData.totalDebtUSD; // U:[FA-13] - - if (!isLiquidatable && _isExpired()) { + if (!isLiquidatable && _isExpired() && collateralDebtData.debt != 0) { isLiquidatable = true; // U:[FA-13] - closeAction = ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT; // U:[FA-14] + isExpired = true; // U:[FA-14] } - if (!isLiquidatable) revert CreditAccountNotLiquidatableException(); // U:[FA-13] + } - uint256 tokensToEnable = _claimWithdrawals({ - action: isEmergency ? ClaimAction.FORCE_CANCEL : ClaimAction.CANCEL, - creditAccount: creditAccount, - to: borrower - }); // U:[FA-15] + collateralDebtData.enabledTokensMask = collateralDebtData.enabledTokensMask.disable(UNDERLYING_TOKEN_MASK); - collateralDebtData.enabledTokensMask = collateralDebtData.enabledTokensMask.enable(tokensToEnable); // U:[FA-15] - } + BalanceWithMask[] memory initialBalances = BalancesLogic.storeBalances({ + creditAccount: creditAccount, + tokensMask: collateralDebtData.enabledTokensMask, + getTokenByMaskFn: _getTokenByMask + }); - if (skipCalls < calls.length) { - FullCheckParams memory fullCheckParams = _multicall( - creditAccount, calls, collateralDebtData.enabledTokensMask, CLOSE_CREDIT_ACCOUNT_FLAGS, skipCalls - ); // U:[FA-16] - collateralDebtData.enabledTokensMask = fullCheckParams.enabledTokensMaskAfter; // U:[FA-16] - } + FullCheckParams memory fullCheckParams = _multicall( + creditAccount, calls, collateralDebtData.enabledTokensMask, LIQUIDATE_CREDIT_ACCOUNT_FLAGS, skipCalls + ); // U:[FA-16] + collateralDebtData.enabledTokensMask &= fullCheckParams.enabledTokensMaskAfter; + + bool success = BalancesLogic.compareBalances({ + creditAccount: creditAccount, + tokensMask: collateralDebtData.enabledTokensMask, + balances: initialBalances, + comparison: Comparison.LESS + }); + if (!success) revert RemainingTokenBalanceIncreasedException(); // U:[FA-16] - _eraseAllBotPermissions({creditAccount: creditAccount}); // U:[FA-16] + collateralDebtData.enabledTokensMask = collateralDebtData.enabledTokensMask.enable(UNDERLYING_TOKEN_MASK); - (uint256 remainingFunds, uint256 reportedLoss) = _closeCreditAccount({ + (uint256 remainingFunds, uint256 reportedLoss) = ICreditManagerV3(creditManager).liquidateCreditAccount({ creditAccount: creditAccount, - closureAction: closeAction, collateralDebtData: collateralDebtData, - payer: msg.sender, to: to, - skipTokensMask: skipTokenMask, - convertToETH: convertToETH + isExpired: isExpired }); // U:[FA-16] - if (reportedLoss > 0) { + emit LiquidateCreditAccount(creditAccount, borrower, msg.sender, to, remainingFunds); // U:[FA-14,16,17] + + if (reportedLoss != 0) { maxDebtPerBlockMultiplier = 0; // U:[FA-17] // both cast and addition are safe because amounts are of much smaller scale @@ -387,12 +357,6 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { _pause(); // U:[FA-17] } } - - if (convertToETH) { - _wethWithdrawTo(to); // U:[FA-16] - } - - emit LiquidateCreditAccount(creditAccount, borrower, msg.sender, to, closeAction, remainingFunds); // U:[FA-14,16,17] } /// @notice Executes a batch of calls allowing user to manage their credit account @@ -417,8 +381,8 @@ 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; also, - /// unless caller is a special DAO-approved bot, it is allowed to call `payBot` to receive a payment) + /// - 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) /// - Runs the collateral check /// @param creditAccount Account to perform the calls on /// @param calls List of calls to perform @@ -432,9 +396,9 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { nonReentrant // U:[FA-4] { (uint256 botPermissions, bool forbidden, bool hasSpecialPermissions) = IBotListV3(botList).getBotStatus({ + bot: msg.sender, creditManager: creditManager, - creditAccount: creditAccount, - bot: msg.sender + creditAccount: creditAccount }); if ( @@ -444,57 +408,30 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { revert NotApprovedBotException(); // U:[FA-19] } - if (!hasSpecialPermissions) { - botPermissions = botPermissions.enable(PAY_BOT_CAN_BE_CALLED); - } - _multicallFullCollateralCheck(creditAccount, calls, botPermissions); // U:[FA-19, 20] } - /// @notice Claims all mature delayed withdrawals from `creditAccount` to `to` - /// @param creditAccount Account to claim withdrawals from - /// @param to Address to send the tokens to - /// @dev Reverts if credit facade is paused - /// @dev Reverts if caller is not `creditAccount`'s owner - function claimWithdrawals(address creditAccount, address to) - external - override - creditAccountOwnerOnly(creditAccount) // U:[FA-5] - whenNotPaused // U:[FA-2] - nonReentrant // U:[FA-4] - { - _claimWithdrawals(creditAccount, to, ClaimAction.CLAIM); // U:[FA-40] - } - - /// @notice Sets bot permissions to manage `creditAccount` as well as funding parameters + /// @notice Sets `bot`'s permissions to manage `creditAccount` /// @param creditAccount Account to set permissions for /// @param bot Bot to set permissions for /// @param permissions A bit mask encoding bot permissions - /// @param totalFundingAllowance Total amount of WETH available to bot for payments - /// @param weeklyFundingAllowance Amount of WETH available to bot for payments weekly /// @dev Reverts if caller is not `creditAccount`'s owner + /// @dev Reverts if `permissions` has unexpected bits enabled /// @dev Reverts if account has more active bots than allowed after changing permissions - // to prevent users from inflating liquidation gas costs /// @dev Changes account's `BOT_PERMISSIONS_SET_FLAG` in the credit manager if needed - function setBotPermissions( - address creditAccount, - address bot, - uint192 permissions, - uint72 totalFundingAllowance, - uint72 weeklyFundingAllowance - ) + function setBotPermissions(address creditAccount, address bot, uint192 permissions) external override creditAccountOwnerOnly(creditAccount) // U:[FA-5] nonReentrant // U:[FA-4] { + if (permissions & ~ALL_PERMISSIONS != 0) revert UnexpectedPermissionsException(); // U:[FA-41] + uint256 remainingBots = IBotListV3(botList).setBotPermissions({ + bot: bot, creditManager: creditManager, creditAccount: creditAccount, - bot: bot, - permissions: permissions, - totalFundingAllowance: totalFundingAllowance, - weeklyFundingAllowance: weeklyFundingAllowance + permissions: permissions }); // U:[FA-41] if (remainingBots > maxApprovedBots) { @@ -514,12 +451,11 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { /// @dev Batches price feed updates, multicall and collateral check into a single function function _multicallFullCollateralCheck(address creditAccount, MultiCall[] calldata calls, uint256 flags) internal { - uint256 _forbiddenTokenMask = forbiddenTokenMask; - uint256 enabledTokensMaskBefore = ICreditManagerV3(creditManager).enabledTokensMaskOf(creditAccount); // U:[FA-18] - BalanceWithMask[] memory forbiddenBalances = BalancesLogic.storeForbiddenBalances({ + uint256 forbiddenTokensMask = forbiddenTokenMask; + uint256 enabledTokensMaskBefore = _enabledTokensMaskOf(creditAccount); // U:[FA-18] + BalanceWithMask[] memory forbiddenBalances = BalancesLogic.storeBalances({ creditAccount: creditAccount, - forbiddenTokenMask: _forbiddenTokenMask, - enabledTokensMask: enabledTokensMaskBefore, + tokensMask: forbiddenTokensMask & enabledTokensMaskBefore, getTokenByMaskFn: _getTokenByMask }); @@ -537,7 +473,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { enabledTokensMaskBefore: enabledTokensMaskBefore, fullCheckParams: fullCheckParams, forbiddenBalances: forbiddenBalances, - _forbiddenTokenMask: _forbiddenTokenMask + forbiddenTokensMask: forbiddenTokensMask }); // U:[FA-18] } @@ -587,7 +523,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { else if (method == ICreditFacadeV3Multicall.addCollateral.selector) { _revertIfNoPermission(flags, ADD_COLLATERAL_PERMISSION); // U:[FA-21] - quotedTokensMaskInverted = _getInvertedQuotedTokensMask(quotedTokensMaskInverted); + quotedTokensMaskInverted = _quotedTokensMaskInvertedLoE(quotedTokensMaskInverted); enabledTokensMask = enabledTokensMask.enable({ bitsToEnable: _addCollateral(creditAccount, mcall.callData[4:]), @@ -598,7 +534,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { else if (method == ICreditFacadeV3Multicall.addCollateralWithPermit.selector) { _revertIfNoPermission(flags, ADD_COLLATERAL_PERMISSION); // U:[FA-21] - quotedTokensMaskInverted = _getInvertedQuotedTokensMask(quotedTokensMaskInverted); + quotedTokensMaskInverted = _quotedTokensMaskInvertedLoE(quotedTokensMaskInverted); enabledTokensMask = enabledTokensMask.enable({ bitsToEnable: _addCollateralWithPermit(creditAccount, mcall.callData[4:]), @@ -613,15 +549,16 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { _updateQuota(creditAccount, mcall.callData[4:], flags & FORBIDDEN_TOKENS_BEFORE_CALLS != 0); // U:[FA-34] enabledTokensMask = enabledTokensMask.enableDisable(tokensToEnable, tokensToDisable); // U:[FA-34] } - // scheduleWithdrawal - else if (method == ICreditFacadeV3Multicall.scheduleWithdrawal.selector) { - _revertIfNoPermission(flags, WITHDRAW_PERMISSION); // U:[FA-21] + // withdrawCollateral + else if (method == ICreditFacadeV3Multicall.withdrawCollateral.selector) { + _revertIfNoPermission(flags, WITHDRAW_COLLATERAL_PERMISSION); // U:[FA-21] - flags = flags.enable(REVERT_ON_FORBIDDEN_TOKENS_AFTER_CALLS); // U:[FA-30] + fullCheckParams.revertOnForbiddenTokens = true; // U:[FA-30] + fullCheckParams.useSafePrices = true; - uint256 tokensToDisable = _scheduleWithdrawal(creditAccount, mcall.callData[4:]); // U:[FA-34] + uint256 tokensToDisable = _withdrawCollateral(creditAccount, mcall.callData[4:]); // U:[FA-34] - quotedTokensMaskInverted = _getInvertedQuotedTokensMask(quotedTokensMaskInverted); + quotedTokensMaskInverted = _quotedTokensMaskInvertedLoE(quotedTokensMaskInverted); enabledTokensMask = enabledTokensMask.disable({ bitsToDisable: tokensToDisable, @@ -632,7 +569,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { else if (method == ICreditFacadeV3Multicall.increaseDebt.selector) { _revertIfNoPermission(flags, INCREASE_DEBT_PERMISSION); // U:[FA-21] - flags = flags.enable(REVERT_ON_FORBIDDEN_TOKENS_AFTER_CALLS); // U:[FA-30] + fullCheckParams.revertOnForbiddenTokens = true; // U:[FA-30] (uint256 tokensToEnable,) = _manageDebt( creditAccount, mcall.callData[4:], enabledTokensMask, ManageDebtAction.INCREASE_DEBT @@ -648,12 +585,6 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { ); // U:[FA-31] enabledTokensMask = enabledTokensMask.disable(tokensToDisable); // U:[FA-31] } - // payBot - else if (method == ICreditFacadeV3Multicall.payBot.selector) { - _revertIfNoPermission(flags, PAY_BOT_CAN_BE_CALLED); // U:[FA-21] - flags = flags.disable(PAY_BOT_CAN_BE_CALLED); // U:[FA-37] - _payBot(creditAccount, mcall.callData[4:]); // U:[FA-37] - } // setFullCheckParams else if (method == ICreditFacadeV3Multicall.setFullCheckParams.selector) { (fullCheckParams.collateralHints, fullCheckParams.minHealthFactor) = @@ -664,7 +595,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { _revertIfNoPermission(flags, ENABLE_TOKEN_PERMISSION); // U:[FA-21] address token = abi.decode(mcall.callData[4:], (address)); // U:[FA-33] - quotedTokensMaskInverted = _getInvertedQuotedTokensMask(quotedTokensMaskInverted); + quotedTokensMaskInverted = _quotedTokensMaskInvertedLoE(quotedTokensMaskInverted); enabledTokensMask = enabledTokensMask.enable({ bitsToEnable: _getTokenMaskOrRevert(token), @@ -676,7 +607,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { _revertIfNoPermission(flags, DISABLE_TOKEN_PERMISSION); // U:[FA-21] address token = abi.decode(mcall.callData[4:], (address)); // U:[FA-33] - quotedTokensMaskInverted = _getInvertedQuotedTokensMask(quotedTokensMaskInverted); + quotedTokensMaskInverted = _quotedTokensMaskInvertedLoE(quotedTokensMaskInverted); enabledTokensMask = enabledTokensMask.disable({ bitsToDisable: _getTokenMaskOrRevert(token), @@ -716,7 +647,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { (uint256 tokensToEnable, uint256 tokensToDisable) = abi.decode(result, (uint256, uint256)); // U:[FA-38] - quotedTokensMaskInverted = _getInvertedQuotedTokensMask(quotedTokensMaskInverted); + quotedTokensMaskInverted = _quotedTokensMaskInvertedLoE(quotedTokensMaskInverted); enabledTokensMask = enabledTokensMask.enableDisable({ bitsToEnable: tokensToEnable, @@ -728,12 +659,16 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { } if (expectedBalances.length != 0) { - bool success = BalancesLogic.compareBalances(creditAccount, expectedBalances); + bool success = BalancesLogic.compareBalances({ + creditAccount: creditAccount, + balances: expectedBalances, + comparison: Comparison.GREATER + }); if (!success) revert BalanceLessThanMinimumDesiredException(); // U:[FA-23] } - if ((flags & REVERT_ON_FORBIDDEN_TOKENS_AFTER_CALLS != 0) && (enabledTokensMask & forbiddenTokenMask != 0)) { - revert ForbiddenTokensException(); // U:[FA-27] + if (enabledTokensMask & forbiddenTokenMask != 0) { + fullCheckParams.useSafePrices = true; } if (flags & EXTERNAL_CONTRACT_WAS_CALLED != 0) { @@ -757,10 +692,12 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { mcall.target == address(this) && bytes4(mcall.callData) == ICreditFacadeV3Multicall.onDemandPriceUpdate.selector ) { - (address token, bytes memory data) = abi.decode(mcall.callData[4:], (address, bytes)); // U:[FA-25] + (address token, bool reserve, bytes memory data) = + abi.decode(mcall.callData[4:], (address, bool, bytes)); // U:[FA-25] + + priceOracle = _priceOracleLoE(priceOracle); // U:[FA-25] + address priceFeed = IPriceOracleV3(priceOracle).priceFeedsRaw(token, reserve); // U:[FA-25] - priceOracle = _getPriceOracle(priceOracle); // U:[FA-25] - address priceFeed = IPriceOracleBase(priceOracle).priceFeeds(token); // U:[FA-25] if (priceFeed == address(0)) { revert PriceFeedDoesNotExistException(); // U:[FA-25] } @@ -776,6 +713,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { /// @dev Performs collateral check to ensure that /// - account is sufficiently collateralized + /// - account has no forbidden tokens after risky operations /// - no forbidden tokens have been enabled during the multicall /// - no enabled forbidden token balance has increased during the multicall function _fullCollateralCheck( @@ -783,25 +721,34 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { uint256 enabledTokensMaskBefore, FullCheckParams memory fullCheckParams, BalanceWithMask[] memory forbiddenBalances, - uint256 _forbiddenTokenMask + uint256 forbiddenTokensMask ) internal { - uint256 enabledTokensMaskUpdated = ICreditManagerV3(creditManager).fullCollateralCheck( + uint256 enabledTokensMask = ICreditManagerV3(creditManager).fullCollateralCheck( creditAccount, fullCheckParams.enabledTokensMaskAfter, fullCheckParams.collateralHints, - fullCheckParams.minHealthFactor + fullCheckParams.minHealthFactor, + fullCheckParams.useSafePrices ); - bool success = BalancesLogic.checkForbiddenBalances({ - creditAccount: creditAccount, - enabledTokensMaskBefore: enabledTokensMaskBefore, - enabledTokensMaskAfter: enabledTokensMaskUpdated, - forbiddenBalances: forbiddenBalances, - forbiddenTokenMask: _forbiddenTokenMask - }); - if (!success) revert ForbiddenTokensException(); // U:[FA-30] + uint256 enabledForbiddenTokensMask = enabledTokensMask & forbiddenTokensMask; + if (enabledForbiddenTokensMask != 0) { + if (fullCheckParams.revertOnForbiddenTokens) revert ForbiddenTokensException(); + + uint256 enabledForbiddenTokensMaskBefore = enabledTokensMaskBefore & forbiddenTokensMask; + if (enabledForbiddenTokensMask & ~enabledForbiddenTokensMaskBefore != 0) { + revert ForbiddenTokenEnabledException(); + } + + bool success = BalancesLogic.compareBalances({ + creditAccount: creditAccount, + tokensMask: enabledForbiddenTokensMask, + balances: forbiddenBalances, + comparison: Comparison.LESS + }); - emit SetEnabledTokensMask(creditAccount, enabledTokensMaskUpdated); + if (!success) revert ForbiddenTokenBalanceIncreasedException(); + } } /// @dev `ICreditFacadeV3Multicall.addCollateral` implementation @@ -881,14 +828,23 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { }); // U:[FA-34] } - /// @dev `ICreditFacadeV3Multicall.scheduleWithdrawal` implementation - function _scheduleWithdrawal(address creditAccount, bytes calldata callData) + /// @dev `ICreditFacadeV3Multicall.withdrawCollateral` implementation + function _withdrawCollateral(address creditAccount, bytes calldata callData) internal returns (uint256 tokensToDisable) { - (address token, uint256 amount) = abi.decode(callData, (address, uint256)); // U:[FA-35] + (address token, uint256 amount, address to) = abi.decode(callData, (address, uint256, address)); // U:[FA-35] + + if (amount == type(uint256).max) { + amount = IERC20(token).balanceOf(creditAccount); + if (amount <= 1) return 0; + unchecked { + --amount; + } + } + tokensToDisable = ICreditManagerV3(creditManager).withdrawCollateral(creditAccount, token, amount, to); // U:[FA-35] - tokensToDisable = ICreditManagerV3(creditManager).scheduleWithdrawal(creditAccount, token, amount); // U:[FA-35] + emit WithdrawCollateral(creditAccount, token, amount); // U:[FA-35] } /// @dev `ICreditFacadeV3Multicall.revokeAdapterAllowances` implementation @@ -898,20 +854,6 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { ICreditManagerV3(creditManager).revokeAdapterAllowances(creditAccount, revocations); // U:[FA-36] } - /// @dev `ICreditFacadeV3Multicall.payBot` implementation - function _payBot(address creditAccount, bytes calldata callData) internal { - uint72 paymentAmount = abi.decode(callData, (uint72)); - address payer = _getBorrowerOrRevert(creditAccount); // U:[FA-37] - - IBotListV3(botList).payBot({ - payer: payer, - creditManager: creditManager, - creditAccount: creditAccount, - bot: msg.sender, - paymentAmount: paymentAmount - }); // U:[FA-37] - } - // ------------- // // CONFIGURATION // // ------------- // @@ -1056,16 +998,20 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { } } - /// @dev Returns inverted quoted tokens mask, avoids external call if it has already been queried - function _getInvertedQuotedTokensMask(uint256 currentMask) internal view returns (uint256) { - // since underlying token can't be quoted, we can use `currentMask == 0` as an indicator - // that mask hasn't been queried yet - return currentMask == 0 ? ~ICreditManagerV3(creditManager).quotedTokensMask() : currentMask; + /// @dev Load-on-empty function to read inverted quoted tokens mask at most once if it's needed, + /// returns its argument if it's not empty or inverted `quotedTokensMask` from credit manager otherwise + /// @dev Non-empty inverted quoted tokens mask always has it's LSB set to 1 since underlying can't be quoted + function _quotedTokensMaskInvertedLoE(uint256 quotedTokensMaskInvertedOrEmpty) internal view returns (uint256) { + return quotedTokensMaskInvertedOrEmpty == 0 + ? ~ICreditManagerV3(creditManager).quotedTokensMask() + : quotedTokensMaskInvertedOrEmpty; } - /// @dev Returns price oracle address, avoids external call if it has already been queried - function _getPriceOracle(address priceOracle) internal view returns (address) { - return priceOracle == address(0) ? ICreditManagerV3(creditManager).priceOracle() : priceOracle; + /// @dev Load-on-empty function to read price oracle at most once if it's needed, + /// returns its argument if it's not empty or `priceOracle` from credit manager otherwise + /// @dev Non-empty price oracle always has non-zero address + function _priceOracleLoE(address priceOracleOrEmpty) internal view returns (address) { + return priceOracleOrEmpty == address(0) ? ICreditManagerV3(creditManager).priceOracle() : priceOracleOrEmpty; } /// @dev Wraps any ETH sent in the function call and sends it back to `msg.sender` @@ -1076,11 +1022,6 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { } } - /// @dev Claims ETH from withdrawal manager, expecting that WETH was deposited there earlier in the transaction - function _wethWithdrawTo(address to) internal { - IWithdrawalManagerV3(withdrawalManager).claimImmediateWithdrawal({token: ETH_ADDRESS, to: to}); - } - /// @dev Whether credit facade has expired (`false` if it's not expirable or expiration timestamp is not set) function _isExpired() internal view returns (bool) { if (!expirable) return false; // U:[FA-46] @@ -1123,27 +1064,6 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { _setActiveCreditAccount(INACTIVE_CREDIT_ACCOUNT_ADDRESS); } - /// @dev Internal wrapper for `creditManager.closeCreditAccount` call to reduce contract size - function _closeCreditAccount( - address creditAccount, - ClosureAction closureAction, - CollateralDebtData memory collateralDebtData, - address payer, - address to, - uint256 skipTokensMask, - bool convertToETH - ) internal returns (uint256 remainingFunds, uint256 reportedLoss) { - (remainingFunds, reportedLoss) = ICreditManagerV3(creditManager).closeCreditAccount({ - creditAccount: creditAccount, - closureAction: closureAction, - collateralDebtData: collateralDebtData, - payer: payer, - to: to, - skipTokensMask: skipTokensMask, - convertToETH: convertToETH - }); - } - /// @dev Internal wrapper for `creditManager.addCollateral` call to reduce contract size function _addCollateral(address payer, address creditAccount, address token, uint256 amount) internal @@ -1157,29 +1077,9 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { }); } - /// @dev Internal wrapper for `creditManager.calcDebtAndCollateral` call to reduce contract size - function _calcDebtAndCollateral(address creditAccount, CollateralCalcTask task) - internal - view - returns (CollateralDebtData memory) - { - return ICreditManagerV3(creditManager).calcDebtAndCollateral(creditAccount, task); - } - - /// @dev Internal wrapper for `creditManager.claimWithdrawals` call to reduce contract size - function _claimWithdrawals(address creditAccount, address to, ClaimAction action) - internal - returns (uint256 tokensToEnable) - { - tokensToEnable = ICreditManagerV3(creditManager).claimWithdrawals(creditAccount, to, action); - } - - /// @dev Internal wrapper for `botList.eraseAllBotPermissions` call to reduce contract size - function _eraseAllBotPermissions(address creditAccount) internal { - uint16 flags = _flagsOf(creditAccount); - if (flags & BOT_PERMISSIONS_SET_FLAG != 0) { - IBotListV3(botList).eraseAllBotPermissions(creditManager, creditAccount); - } + /// @dev Internal wrapper for `creditManager.enabledTokensMaskOf` call to reduce contract size + function _enabledTokensMaskOf(address creditAccount) internal view returns (uint256) { + return ICreditManagerV3(creditManager).enabledTokensMaskOf(creditAccount); } /// @dev Reverts if `msg.sender` is not credit configurator diff --git a/contracts/credit/CreditManagerV3.sol b/contracts/credit/CreditManagerV3.sol index 45e33b8b..06df37bb 100644 --- a/contracts/credit/CreditManagerV3.sol +++ b/contracts/credit/CreditManagerV3.sol @@ -7,6 +7,7 @@ pragma solidity ^0.8.17; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; // LIBS & TRAITS import {UNDERLYING_TOKEN_MASK, BitMask} from "../libraries/BitMask.sol"; @@ -21,22 +22,19 @@ import {SanityCheckTrait} from "../traits/SanityCheckTrait.sol"; import {IAccountFactoryBase} from "../interfaces/IAccountFactoryV3.sol"; import {ICreditAccountBase} from "../interfaces/ICreditAccountV3.sol"; import {IPoolV3} from "../interfaces/IPoolV3.sol"; -import {ClaimAction, IWithdrawalManagerV3} from "../interfaces/IWithdrawalManagerV3.sol"; import { ICreditManagerV3, - ClosureAction, CollateralTokenData, ManageDebtAction, CreditAccountInfo, RevocationPair, CollateralDebtData, CollateralCalcTask, - WITHDRAWAL_FLAG, DEFAULT_MAX_ENABLED_TOKENS, INACTIVE_CREDIT_ACCOUNT_ADDRESS } from "../interfaces/ICreditManagerV3.sol"; import "../interfaces/IAddressProviderV3.sol"; -import {IPriceOracleBase} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracleBase.sol"; +import {IPriceOracleV3} from "../interfaces/IPriceOracleV3.sol"; import {IPoolQuotaKeeperV3} from "../interfaces/IPoolQuotaKeeperV3.sol"; // CONSTANTS @@ -53,6 +51,7 @@ import "../interfaces/IExceptions.sol"; contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardTrait { using EnumerableSet for EnumerableSet.AddressSet; using BitMask for uint256; + using Math for uint256; using CreditLogic for CollateralDebtData; using CollateralLogic for CollateralDebtData; using SafeERC20 for IERC20; @@ -76,9 +75,6 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT /// @notice WETH token address address public immutable override weth; - /// @notice Withdrawal manager contract address - address public immutable override withdrawalManager; - /// @notice Address of the connected credit facade address public override creditFacade; @@ -169,7 +165,6 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT weth = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_WETH_TOKEN, NO_VERSION_CONTROL); // U:[CM-1] priceOracle = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_PRICE_ORACLE, 3_00); // U:[CM-1] accountFactory = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_ACCOUNT_FACTORY, NO_VERSION_CONTROL); // U:[CM-1] - withdrawalManager = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_WITHDRAWAL_MANAGER, 3_00); // U:[CM-1] creditConfigurator = msg.sender; // U:[CM-1] @@ -195,11 +190,6 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT CreditAccountInfo storage newCreditAccountInfo = creditAccountInfo[creditAccount]; - // accounts are reusable, so debt and interest index must be reset either when opening an account or closing it - // to make potential liquidations cheaper, they are reset here - newCreditAccountInfo.debt = 0; // U:[CM-6] - newCreditAccountInfo.cumulativeIndexLastUpdate = _poolBaseInterestIndex(); // U:[CM-6] - // newCreditAccountInfo.flags = 0; // newCreditAccountInfo.lastDebtUpdate = 0; // newCreditAccountInfo.borrower = onBehalfOf; @@ -219,29 +209,53 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT creditAccountsSet.add(creditAccount); // U:[CM-6] } - /// @notice Closes a credit account by repaying debt to the pool, removing quotas and returning account to the factory + /// @notice Closes a credit account /// @param creditAccount Account to close - /// @param closureAction Closure mode, see `ClosureAction` for details + /// @custom:expects Account is opened in this credit manager + function closeCreditAccount(address creditAccount) + external + override + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] + { + CreditAccountInfo storage currentCreditAccountInfo = creditAccountInfo[creditAccount]; + if (currentCreditAccountInfo.debt != 0) { + revert CloseAccountWithNonZeroDebtException(); // U:[CM-7] + } + + // currentCreditAccountInfo.borrower = address(0); + // currentCreditAccountInfo.lastDebtUpdate = 0; + // currentCreditAccountInfo.flags = 0; + assembly { + let slot := add(currentCreditAccountInfo.slot, 4) + sstore(slot, 0) + } // U:[CM-7] + + currentCreditAccountInfo.enabledTokensMask = 0; // U:[CM-7] + + IAccountFactoryBase(accountFactory).returnCreditAccount({creditAccount: creditAccount}); // U:[CM-7] + creditAccountsSet.remove(creditAccount); // U:[CM-7] + } + + /// @notice Liquidates a credit account + /// - Removes account's quotas, and, if there's loss incurred on liquidation, + /// also zeros out limits for account's quoted tokens in the quota keeper + /// - Repays debt to the pool + /// - Ensures that the value of funds remaining on the account is sufficient + /// - Transfers underlying surplus (if any) to the liquidator + /// - Resets account's debt, quota interest and fees to zero + /// @param creditAccount Account to liquidate /// @param collateralDebtData A struct with account's debt and collateral data - /// @param payer Address to transfer underlying from in case account's balance is insufficient - /// @param to Address to transfer tokens that left on the account after closure and repayments - /// @param skipTokensMask Bit mask of tokens that should be skipped - /// @param convertToETH If true and any of transferred tokens is WETH, it will be sent to withdrawal manager, - /// from which `to` can later claim it as ETH - /// @return remainingFunds Amount of underlying sent to account owner on liquidation + /// @param to Address to transfer underlying left after liquidation + /// @return remainingFunds Total value of assets left on the account after liquidation /// @return loss Loss incurred on liquidation - /// @dev If `loss > 0`, zeroes out limits for account's quoted tokens in the quota keeper - /// @custom:expects `cdd` is a result of `calcDebtAndCollateral` in `DEBT_COLLATERAL_{x}_WITHDRAWALS` mode, where x is - /// `WITHOUT` for normal closure, `CANCEL` for liquidation and `FORCE_CANCEL` for emergency liquidation - /// @custom:invariant `remainingFunds * loss == 0` - function closeCreditAccount( + /// @custom:expects Account is opened in this credit manager + /// @custom:expects `collateralDebtData` is a result of `calcDebtAndCollateral` in `DEBT_COLLATERAL` mode + function liquidateCreditAccount( address creditAccount, - ClosureAction closureAction, CollateralDebtData calldata collateralDebtData, - address payer, address to, - uint256 skipTokensMask, - bool convertToETH + bool isExpired ) external override @@ -249,90 +263,63 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT creditFacadeOnly // U:[CM-2] returns (uint256 remainingFunds, uint256 loss) { - address borrower = getBorrowerOrRevert(creditAccount); // U:[CM-7] - - { - CreditAccountInfo storage currentCreditAccountInfo = creditAccountInfo[creditAccount]; - if (currentCreditAccountInfo.lastDebtUpdate == block.number) { - revert DebtUpdatedTwiceInOneBlockException(); // U:[CM-7] - } - - // currentCreditAccountInfo.borrower = address(0); - // currentCreditAccountInfo.lastDebtUpdate = 0; - // currentCreditAccountInfo.flags = 0; - assembly { - let slot := add(currentCreditAccountInfo.slot, 4) - sstore(slot, 0) - } // U:[CM-8] - } - uint256 amountToPool; + uint256 minRemainingFunds; uint256 profit; - if (closureAction == ClosureAction.CLOSE_ACCOUNT) { - (amountToPool, profit) = collateralDebtData.calcClosePayments({amountWithFeeFn: _amountWithFee}); // U:[CM-8] - } else { - bool isNormalLiquidation = closureAction == ClosureAction.LIQUIDATE_ACCOUNT; - - (amountToPool, remainingFunds, profit, loss) = collateralDebtData.calcLiquidationPayments({ - liquidationDiscount: isNormalLiquidation ? liquidationDiscount : liquidationDiscountExpired, - feeLiquidation: isNormalLiquidation ? feeLiquidation : feeLiquidationExpired, - amountWithFeeFn: _amountWithFee, - amountMinusFeeFn: _amountMinusFee - }); // U:[CM-8] - } - - { - uint256 underlyingBalance = IERC20(underlying).safeBalanceOf({account: creditAccount}); // U:[CM-8] - uint256 distributedFunds = amountToPool + remainingFunds + 1; - - if (underlyingBalance < distributedFunds) { - unchecked { - IERC20(underlying).safeTransferFrom({ - from: payer, - to: creditAccount, - amount: _amountWithFee(distributedFunds - underlyingBalance) - }); // U:[CM-8] - } - } - } + (amountToPool, minRemainingFunds, profit, loss) = collateralDebtData.calcLiquidationPayments({ + liquidationDiscount: isExpired ? liquidationDiscountExpired : liquidationDiscount, + feeLiquidation: isExpired ? feeLiquidationExpired : feeLiquidation, + amountWithFeeFn: _amountWithFee, + amountMinusFeeFn: _amountMinusFee + }); // U:[CM-8] if (collateralDebtData.quotedTokens.length != 0) { - bool setLimitsToZero = loss > 0; // U:[CM-8] - IPoolQuotaKeeperV3(collateralDebtData._poolQuotaKeeper).removeQuotas({ creditAccount: creditAccount, tokens: collateralDebtData.quotedTokens, - setLimitsToZero: setLimitsToZero + setLimitsToZero: loss > 0 }); // U:[CM-8] } if (amountToPool != 0) { ICreditAccountBase(creditAccount).transfer({token: underlying, to: pool, amount: amountToPool}); // U:[CM-8] } + _poolRepayCreditAccount(collateralDebtData.debt, profit, loss); // U:[CM-8] + + uint256 underlyingBalance; + (remainingFunds, underlyingBalance) = + _getRemainingFunds({creditAccount: creditAccount, enabledTokensMask: collateralDebtData.enabledTokensMask}); // U:[CM-8] + + if (remainingFunds < minRemainingFunds) { + revert InsufficientRemainingFundsException(); // U:[CM-8] + } + + unchecked { + uint256 amountToLiquidator = Math.min(remainingFunds - minRemainingFunds, underlyingBalance); + + if (amountToLiquidator != 0) { + ICreditAccountBase(creditAccount).transfer({token: underlying, to: to, amount: amountToLiquidator}); // U:[CM-8] - if (collateralDebtData.debt + profit + loss != 0) { - _poolRepayCreditAccount(collateralDebtData.debt, profit, loss); // U:[CM-8] + remainingFunds -= amountToLiquidator; // U:[CM-8] + } } - if (remainingFunds > 1) { - _safeTokenTransfer({ - creditAccount: creditAccount, - token: underlying, - to: borrower, - amount: remainingFunds, - convertToETH: false - }); // U:[CM-8] + CreditAccountInfo storage currentCreditAccountInfo = creditAccountInfo[creditAccount]; + if (currentCreditAccountInfo.lastDebtUpdate == block.number) { + revert DebtUpdatedTwiceInOneBlockException(); // U:[CM-9] } - _batchTokensTransfer({ - creditAccount: creditAccount, - to: to, - convertToETH: convertToETH, - tokensToTransferMask: collateralDebtData.enabledTokensMask.disable(skipTokensMask) - }); // U:[CM-8, 9] + currentCreditAccountInfo.debt = 0; // U:[CM-8] + currentCreditAccountInfo.lastDebtUpdate = uint64(block.number); // U:[CM-8] + currentCreditAccountInfo.enabledTokensMask = + collateralDebtData.enabledTokensMask.disable(collateralDebtData.quotedTokensMask); // U:[CM-8] - IAccountFactoryBase(accountFactory).returnCreditAccount({creditAccount: creditAccount}); // U:[CM-8] - creditAccountsSet.remove(creditAccount); // U:[CM-8] + // currentCreditAccountInfo.cumulativeQuotaInterest = 1; + // currentCreditAccountInfo.quotaFees = 0; + assembly { + let slot := add(currentCreditAccountInfo.slot, 2) + sstore(slot, 1) + } // U:[CM-8] } /// @notice Increases or decreases credit account's debt @@ -366,7 +353,8 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT minHealthFactor: PERCENTAGE_FACTOR, task: (action == ManageDebtAction.INCREASE_DEBT) ? CollateralCalcTask.GENERIC_PARAMS - : CollateralCalcTask.DEBT_ONLY + : CollateralCalcTask.DEBT_ONLY, + useSafePrices: false }); // U:[CM-10, 11] uint256 newCumulativeIndex; @@ -442,7 +430,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT /// @param creditAccount Account to add collateral to /// @param token Token to add as collateral /// @param amount Amount to add - /// @return tokenMask Mask of the added token + /// @return tokensToEnable Mask of tokens that should be enabled after the operation (always `token` mask) /// @dev Requires approval for `token` from `payer` to this contract /// @dev Reverts if `token` is not recognized as collateral in the credit manager function addCollateral(address payer, address creditAccount, address token, uint256 amount) @@ -450,12 +438,58 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT override nonReentrant // U:[CM-5] creditFacadeOnly // U:[CM-2] - returns (uint256 tokenMask) + returns (uint256 tokensToEnable) { - tokenMask = getTokenMaskOrRevert({token: token}); // U:[CM-13] + tokensToEnable = getTokenMaskOrRevert({token: token}); // U:[CM-13] IERC20(token).safeTransferFrom({from: payer, to: creditAccount, amount: amount}); // U:[CM-13] } + /// @notice Withdraws `amount` of `token` collateral from `creditAccount` to `to` + /// @param creditAccount Credit account to withdraw collateral from + /// @param token Token to withdraw + /// @param amount Amount to withdraw + /// @param to Address to transfer token to + /// @return tokensToDisable Mask of tokens that should be disabled after the operation + /// (`token` mask if withdrawing the entire balance, zero otherwise) + /// @dev Reverts if `token` is not recognized as collateral in the credit manager + function withdrawCollateral(address creditAccount, address token, uint256 amount, address to) + external + override + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] + returns (uint256 tokensToDisable) + { + uint256 tokenMask = getTokenMaskOrRevert({token: token}); // U:[CM-26] + + ICreditAccountBase(creditAccount).transfer({token: token, to: to, amount: amount}); // U:[CM-27] + + if (IERC20(token).safeBalanceOf({account: creditAccount}) <= 1) { + tokensToDisable = tokenMask; // U:[CM-27] + } + } + + /// @notice Instructs `creditAccount` to make an external call to target with `callData` + function externalCall(address creditAccount, address target, bytes calldata callData) + external + override + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] + returns (bytes memory result) + { + return _execute(creditAccount, target, callData); + } + + /// @notice Instructs `creditAccount` to approve `amount` of `token` to `spender` + /// @dev Reverts if `token` is not recognized as collateral in the credit manager + function approveToken(address creditAccount, address token, address spender, uint256 amount) + external + override + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] + { + _approveSpender({creditAccount: creditAccount, token: token, spender: spender, amount: amount}); + } + /// @notice Revokes credit account's allowances for specified spender/token pairs /// @param creditAccount Account to revoke allowances for /// @param revocations Array of spender/token pairs @@ -496,12 +530,8 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT nonReentrant // U:[CM-5] { address targetContract = _getTargetContractOrRevert(); // U:[CM-3] - _approveSpender({ - creditAccount: getActiveCreditAccountOrRevert(), - token: token, - spender: targetContract, - amount: amount - }); // U:[CM-14] + address creditAccount = getActiveCreditAccountOrRevert(); // U:[CM-14] + _approveSpender({creditAccount: creditAccount, token: token, spender: targetContract, amount: amount}); // U:[CM-14] } /// @notice Instructs active credit account to call adapter's target contract with provided data @@ -517,7 +547,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT { address targetContract = _getTargetContractOrRevert(); // U:[CM-3] address creditAccount = getActiveCreditAccountOrRevert(); // U:[CM-16] - return ICreditAccountBase(creditAccount).execute(targetContract, data); // U:[CM-16] + return _execute(creditAccount, targetContract, data); // U:[CM-16] } /// @dev Returns adapter's target contract, reverts if `msg.sender` is not a registered adapter @@ -562,6 +592,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT /// @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 Health factor threshold in bps, the check fails if `twvUSD < minHealthFactor * totalDebtUSD` + /// @param useSafePrices Whether to use safe prices when evaluating collateral /// @return enabledTokensMaskAfter Bitmask of account's enabled collateral tokens after potential cleanup /// @dev Reverts if `collateralHints` contains masks that don't correspond to known collateral tokens /// @dev Even when `collateralHints` are specified, quoted tokens are evaluated before non-quoted ones @@ -570,7 +601,8 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT address creditAccount, uint256 enabledTokensMask, uint256[] calldata collateralHints, - uint16 minHealthFactor + uint16 minHealthFactor, + bool useSafePrices ) external override @@ -582,12 +614,21 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT revert CustomHealthFactorTooLowException(); // U:[CM-17] } + unchecked { + uint256 len = collateralHints.length; + for (uint256 i; i < len; ++i) { + uint256 mask = collateralHints[i]; + if (mask == 0 || mask & mask - 1 != 0) revert InvalidCollateralHintException(); // U:[CM-17] + } + } + CollateralDebtData memory cdd = _calcDebtAndCollateral({ creditAccount: creditAccount, minHealthFactor: minHealthFactor, collateralHints: collateralHints, enabledTokensMask: enabledTokensMask, - task: CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY + task: CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY, + useSafePrices: useSafePrices }); // U:[CM-18] if (cdd.twvUSD < cdd.totalDebtUSD) { @@ -608,7 +649,8 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT enabledTokensMask: enabledTokensMaskOf(creditAccount), collateralHints: collateralHints, minHealthFactor: minHealthFactor, - task: CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY + task: CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY, + useSafePrices: false }); // U:[CM-18] return cdd.twvUSD < cdd.totalDebtUSD; // U:[CM-18] @@ -629,13 +671,21 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT revert IncorrectParameterException(); // U:[CM-19] } + bool useSafePrices; + + if (task == CollateralCalcTask.DEBT_COLLATERAL_SAFE_PRICES) { + task = CollateralCalcTask.DEBT_COLLATERAL; + useSafePrices = true; + } + uint256[] memory collateralHints; cdd = _calcDebtAndCollateral({ creditAccount: creditAccount, enabledTokensMask: enabledTokensMaskOf(creditAccount), collateralHints: collateralHints, minHealthFactor: PERCENTAGE_FACTOR, - task: task + task: task, + useSafePrices: useSafePrices }); // U:[CM-20] } @@ -645,19 +695,21 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT /// @param collateralHints Optional array of token masks specifying the order of checking collateral tokens /// @param minHealthFactor Health factor in bps to stop the calculations after when performing collateral check /// @param task Calculation mode, see `CollateralCalcTask` for details + /// @param useSafePrices Whether to use safe prices when evaluating collateral /// @return cdd A struct with debt and collateral data function _calcDebtAndCollateral( address creditAccount, uint256 enabledTokensMask, uint256[] memory collateralHints, uint16 minHealthFactor, - CollateralCalcTask task + CollateralCalcTask task, + bool useSafePrices ) internal view returns (CollateralDebtData memory cdd) { CreditAccountInfo storage currentCreditAccountInfo = creditAccountInfo[creditAccount]; cdd.debt = currentCreditAccountInfo.debt; // U:[CM-20] cdd.cumulativeIndexLastUpdate = currentCreditAccountInfo.cumulativeIndexLastUpdate; // U:[CM-20] - cdd.cumulativeIndexNow = _poolBaseInterestIndex(); // U:[CM-20] + cdd.cumulativeIndexNow = IPoolV3(pool).baseInterestIndex(); // U:[CM-20] if (task == CollateralCalcTask.GENERIC_PARAMS) { return cdd; // U:[CM-20] @@ -691,11 +743,13 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT address _priceOracle = priceOracle; - uint256 totalDebt = cdd.calcTotalDebt(); - if (totalDebt != 0) { - cdd.totalDebtUSD = _convertToUSD({_priceOracle: _priceOracle, amountInToken: totalDebt, token: underlying}); // U:[CM-22] - } else if (task == CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY) { - return cdd; // U:[CM-18A] + { + uint256 totalDebt = cdd.calcTotalDebt(); + if (totalDebt != 0) { + cdd.totalDebtUSD = _convertToUSD(_priceOracle, totalDebt, underlying); // U:[CM-22] + } else if (task == CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY) { + return cdd; // U:[CM-18A] + } } uint256 targetUSD = (task == CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY) @@ -711,7 +765,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT quotasPacked: quotasPacked, priceOracle: _priceOracle, collateralTokenByMaskFn: _collateralTokenByMask, - convertToUSDFn: _convertToUSD + convertToUSDFn: useSafePrices ? _safeConvertToUSD : _convertToUSD }); // U:[CM-22] cdd.enabledTokensMask = enabledTokensMask.disable(tokensToDisable); // U:[CM-22] @@ -719,14 +773,6 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT return cdd; // U:[CM-18] } - if (task != CollateralCalcTask.DEBT_COLLATERAL_WITHOUT_WITHDRAWALS && _hasWithdrawals(creditAccount)) { - cdd.totalValueUSD += _getCancellableWithdrawalsValue({ - _priceOracle: _priceOracle, - creditAccount: creditAccount, - isForceCancel: task == CollateralCalcTask.DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS - }); // U:[CM-23] - } - cdd.totalValue = _convertFromUSD(_priceOracle, cdd.totalValueUSD, underlying); // U:[CM-22,23] } @@ -802,6 +848,41 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT } } + /// @dev Returns total value of funds remaining on the credit account after liquidation, which consists of underlying + /// token balance and total value of other enabled tokens remaining after transferring specified tokens + /// @param creditAccount Account to compute value for + /// @param enabledTokensMask Bit mask of tokens enabled on the account + /// @return remainingFunds Remaining funds denominated in underlying + /// @return underlyingBalance Balance of underlying token + function _getRemainingFunds(address creditAccount, uint256 enabledTokensMask) + internal + view + returns (uint256 remainingFunds, uint256 underlyingBalance) + { + underlyingBalance = IERC20(underlying).safeBalanceOf({account: creditAccount}); + remainingFunds = underlyingBalance; + + uint256 remainingTokensMask = enabledTokensMask.disable(UNDERLYING_TOKEN_MASK); + if (remainingTokensMask == 0) return (remainingFunds, underlyingBalance); + + address _priceOracle = priceOracle; + uint256 totalValueUSD; + while (remainingTokensMask != 0) { + uint256 tokenMask = remainingTokensMask & uint256(-int256(remainingTokensMask)); + remainingTokensMask ^= tokenMask; + + address token = getTokenByMask(tokenMask); + uint256 balance = IERC20(token).safeBalanceOf({account: creditAccount}); + if (balance > 1) { + totalValueUSD += _convertToUSD(_priceOracle, balance, token); + } + } + + if (totalValueUSD != 0) { + remainingFunds += _convertFromUSD(_priceOracle, totalValueUSD, underlying); + } + } + // ------ // // QUOTAS // // ------ // @@ -857,107 +938,6 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT } } - // ----------- // - // WITHDRAWALS // - // ----------- // - - /// @notice Schedules a withdrawal from the credit account - /// @param creditAccount Credit account to schedule a withdrawal from - /// @param token Token to withdraw - /// @param amount Amount to withdraw - /// @return tokensToDisable Mask of tokens that should be disabled after the operation - /// (equals `token`'s mask if withdrawing the entire balance, zero otherwise) - /// @dev If withdrawal manager's delay is zero, token is immediately sent to the account owner. - /// Otherwise, token is sent to the withdrawal manager and `WITHDRAWAL_FLAG` is enabled for the account. - function scheduleWithdrawal(address creditAccount, address token, uint256 amount) - external - override - nonReentrant // U:[CM-5] - creditFacadeOnly // U:[CM-2] - returns (uint256 tokensToDisable) - { - uint256 tokenMask = getTokenMaskOrRevert({token: token}); // U:[CM-26] - - if (IWithdrawalManagerV3(withdrawalManager).delay() == 0) { - address borrower = getBorrowerOrRevert({creditAccount: creditAccount}); - _safeTokenTransfer({ - creditAccount: creditAccount, - token: token, - to: borrower, - amount: amount, - convertToETH: false - }); // U:[CM-27] - } else { - uint256 delivered = ICreditAccountBase(creditAccount).transferDeliveredBalanceControl({ - token: token, - to: withdrawalManager, - amount: amount - }); // U:[CM-28] - - IWithdrawalManagerV3(withdrawalManager).addScheduledWithdrawal({ - creditAccount: creditAccount, - token: token, - amount: delivered, - tokenIndex: tokenMask.calcIndex() - }); // U:[CM-28] - - _enableFlag({creditAccount: creditAccount, flag: WITHDRAWAL_FLAG}); - } - - if (IERC20(token).safeBalanceOf({account: creditAccount}) <= 1) { - tokensToDisable = tokenMask; // U:[CM-27] - } - } - - /// @notice Claims scheduled withdrawals from the credit account - /// @param creditAccount Credit account to claim withdrawals from - /// @param to Address to claim withdrawals to - /// @param action Action to perform, see `ClaimAction` for details - /// @return tokensToEnable Mask of tokens that should be enabled after the operation - /// (non-zero when tokens are returned to the account on withdrawal cancellation) - /// @dev If account has no withdrawals scheduled after the operation, `WITHDRAWAL_FLAG` is disabled - function claimWithdrawals(address creditAccount, address to, ClaimAction action) - external - override - nonReentrant // U:[CM-5] - creditFacadeOnly // U:[CM-2] - returns (uint256 tokensToEnable) - { - if (_hasWithdrawals(creditAccount)) { - bool hasScheduled; - - (hasScheduled, tokensToEnable) = - IWithdrawalManagerV3(withdrawalManager).claimScheduledWithdrawals(creditAccount, to, action); // U:[CM-29] - - if (!hasScheduled) { - _disableFlag(creditAccount, WITHDRAWAL_FLAG); // U:[CM-29] - } - } - } - - /// @dev Returns the USD value of `creditAccount`'s cancellable scheduled withdrawals - /// @param isForceCancel Whether to account for immature or all scheduled withdrawals - function _getCancellableWithdrawalsValue(address _priceOracle, address creditAccount, bool isForceCancel) - internal - view - returns (uint256 totalValueUSD) - { - (address token1, uint256 amount1, address token2, uint256 amount2) = - IWithdrawalManagerV3(withdrawalManager).cancellableScheduledWithdrawals(creditAccount, isForceCancel); // U:[CM-30] - - if (amount1 != 0) { - totalValueUSD = _convertToUSD({_priceOracle: _priceOracle, amountInToken: amount1, token: token1}); // U:[CM-30] - } - if (amount2 != 0) { - totalValueUSD += _convertToUSD({_priceOracle: _priceOracle, amountInToken: amount2, token: token2}); // U:[CM-30] - } - } - - /// @dev Checks whether credit account has scheduled withdrawals - function _hasWithdrawals(address creditAccount) internal view returns (bool) { - return flagsOf(creditAccount) & WITHDRAWAL_FLAG != 0; // U:[CM-36] - } - // --------------------- // // CREDIT MANAGER PARAMS // // --------------------- // @@ -1145,9 +1125,9 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT } /// @dev Saves `creditAccount`'s `enabledTokensMask` in the storage - /// @dev Ensures that the number of enabled tokens does not exceed `maxEnabledTokens` + /// @dev Ensures that the number of enabled tokens excluding underlying does not exceed `maxEnabledTokens` function _saveEnabledTokensMask(address creditAccount, uint256 enabledTokensMask) internal { - if (enabledTokensMask.calcEnabledTokens() > maxEnabledTokens) { + if (enabledTokensMask.disable(UNDERLYING_TOKEN_MASK).calcEnabledTokens() > maxEnabledTokens) { revert TooManyEnabledTokensException(); // U:[CM-37] } @@ -1162,7 +1142,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT /// @notice Returns chunk of up to `limit` credit accounts opened in this credit manager starting from `offset` function creditAccounts(uint256 offset, uint256 limit) external view override returns (address[] memory result) { uint256 len = creditAccountsSet.length(); - uint256 resultLen = offset + limit > len ? len - offset : limit; + uint256 resultLen = offset + limit > len ? (offset > len ? 0 : len - offset) : limit; result = new address[](resultLen); unchecked { @@ -1279,7 +1259,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT override creditConfiguratorOnly // U:[CM-4] { - quotedTokensMask = _quotedTokensMask & ~UNDERLYING_TOKEN_MASK; // U:[CM-43] + quotedTokensMask = _quotedTokensMask.disable(UNDERLYING_TOKEN_MASK); // U:[CM-43] } /// @notice Sets a new max number of enabled tokens @@ -1350,57 +1330,6 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT // INTERNALS // // --------- // - /// @dev Transfers all balances of tokens specified by `tokensToTransferMask` from `creditAccount` to `to` - /// @dev See `_safeTokenTransfer` for additional details - function _batchTokensTransfer(address creditAccount, address to, bool convertToETH, uint256 tokensToTransferMask) - internal - { - unchecked { - while (tokensToTransferMask > 0) { - uint256 tokenMask = tokensToTransferMask & uint256(-int256(tokensToTransferMask)); - tokensToTransferMask &= tokensToTransferMask - 1; - - address token = getTokenByMask(tokenMask); // U:[CM-31] - uint256 amount = IERC20(token).safeBalanceOf({account: creditAccount}); // U:[CM-31] - // 1 wei gas optimization - if (amount > 1) { - _safeTokenTransfer({ - creditAccount: creditAccount, - token: token, - to: to, - amount: amount - 1, - convertToETH: convertToETH - }); // U:[CM-31] - } - } - } - } - - /// @dev Transfers `amount` of `token` from `creditAccount` to `to` - /// @dev If `convertToETH` is true and `token` is WETH, it will be transferred to the withdrawal manager, - /// from which the caller can later claim it as ETH to an arbitrary address - /// @dev If transfer fails, the token will be transferred to withdrawal manager from which `to` - /// can later claim it to an arbitrary address (can be helpful for blacklistable tokens) - function _safeTokenTransfer(address creditAccount, address token, address to, uint256 amount, bool convertToETH) - internal - { - if (convertToETH && token == weth) { - ICreditAccountBase(creditAccount).transfer({token: token, to: withdrawalManager, amount: amount}); // U:[CM-31, 32] - _addImmediateWithdrawal({token: token, to: msg.sender, amount: amount}); // U:[CM-31, 32] - } else { - try ICreditAccountBase(creditAccount).safeTransfer({token: token, to: to, amount: amount}) { - // U:[CM-31, 32, 33] - } catch { - uint256 delivered = ICreditAccountBase(creditAccount).transferDeliveredBalanceControl({ - token: token, - to: withdrawalManager, - amount: amount - }); // U:[CM-33] - _addImmediateWithdrawal({token: token, to: to, amount: delivered}); // U:[CM-33] - } - } - } - /// @dev Approves `amount` of `token` from `creditAccount` to `spender` /// @dev Reverts if `token` is not recognized as collateral in the credit manager function _approveSpender(address creditAccount, address token, address spender, uint256 amount) internal { @@ -1420,9 +1349,9 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT return amount; } - /// @dev Internal wrapper for `pool.baseInterestIndex` call to reduce contract size - function _poolBaseInterestIndex() internal view returns (uint256) { - return IPoolV3(pool).baseInterestIndex(); + /// @dev Internal wrapper for `creditAccount.execute` call to reduce contract size + function _execute(address creditAccount, address target, bytes calldata callData) internal returns (bytes memory) { + return ICreditAccountBase(creditAccount).execute(target, callData); } /// @dev Internal wrapper for `pool.repayCreditAccount` call to reduce contract size @@ -1441,7 +1370,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT view returns (uint256 amountInUSD) { - amountInUSD = IPriceOracleBase(_priceOracle).convertToUSD(amountInToken, token); + amountInUSD = IPriceOracleV3(_priceOracle).convertToUSD(amountInToken, token); } /// @dev Internal wrapper for `priceOracle.convertFromUSD` call to reduce contract size @@ -1450,12 +1379,19 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT view returns (uint256 amountInToken) { - amountInToken = IPriceOracleBase(_priceOracle).convertFromUSD(amountInUSD, token); + amountInToken = IPriceOracleV3(_priceOracle).convertFromUSD(amountInUSD, token); } - /// @dev Internal wrapper for `withdrawalManager.addImmediateWithdrawal` call to reduce contract size - function _addImmediateWithdrawal(address token, address to, uint256 amount) internal { - IWithdrawalManagerV3(withdrawalManager).addImmediateWithdrawal({token: token, to: to, amount: amount}); + /// @dev Internal wrapper for `priceOracle.safeConvertToUSD` call to reduce contract size + /// @dev `underlying` is always converted with default conversion function + function _safeConvertToUSD(address _priceOracle, uint256 amountInToken, address token) + internal + view + returns (uint256 amountInUSD) + { + amountInUSD = (token == underlying) + ? _convertToUSD(_priceOracle, amountInToken, token) + : IPriceOracleV3(_priceOracle).safeConvertToUSD(amountInToken, token); } /// @dev Reverts if `msg.sender` is not the credit facade diff --git a/contracts/governance/ControllerTimelockV3.sol b/contracts/governance/ControllerTimelockV3.sol index ea27e1c8..40b6adb2 100644 --- a/contracts/governance/ControllerTimelockV3.sol +++ b/contracts/governance/ControllerTimelockV3.sol @@ -286,16 +286,16 @@ contract ControllerTimelockV3 is PolicyManagerV3, IControllerTimelockV3 { signature: "rampLiquidationThreshold(address,uint16,uint40,uint24)", data: abi.encode(token, liquidationThresholdFinal, rampStart, rampDuration), delay: delay, - sanityCheckValue: getLTRampParamsHash(creditManager, token), + sanityCheckValue: uint256(getLTRampParamsHash(creditManager, token)), sanityCheckCallData: abi.encodeCall(this.getLTRampParamsHash, (creditManager, token)) }); // U: [CT-6] } /// @dev Retrives the keccak of liquidation threshold params for a token - function getLTRampParamsHash(address creditManager, address token) public view returns (uint256) { + function getLTRampParamsHash(address creditManager, address token) public view returns (bytes32) { (uint16 ltInitial, uint16 ltFinal, uint40 timestampRampStart, uint24 rampDuration) = ICreditManagerV3(creditManager).ltParams(token); - return uint256(keccak256(abi.encode(ltInitial, ltFinal, timestampRampStart, rampDuration))); + return keccak256(abi.encode(ltInitial, ltFinal, timestampRampStart, rampDuration)); } /// @notice Queues a transaction to forbid a third party contract adapter @@ -637,9 +637,9 @@ contract ControllerTimelockV3 is PolicyManagerV3, IControllerTimelockV3 { revert TxExecutedOutsideTimeWindowException(); // U: [CT-9] } - /// In order to ensure that we do not accidentally override a change - /// made by configurator or another admin, the current value of the parameter - /// is compared to the value at the moment of tx being queued + // In order to ensure that we do not accidentally override a change + // made by configurator or another admin, the current value of the parameter + // is compared to the value at the moment of tx being queued if (qtd.sanityCheckCallData.length != 0) { (, bytes memory returndata) = address(this).staticcall(qtd.sanityCheckCallData); diff --git a/contracts/governance/GaugeV3.sol b/contracts/governance/GaugeV3.sol index 8a1a9958..ee35de50 100644 --- a/contracts/governance/GaugeV3.sol +++ b/contracts/governance/GaugeV3.sol @@ -65,6 +65,7 @@ contract GaugeV3 is IGaugeV3, ACLNonReentrantTrait { voter = _gearStaking; // U:[GA-01] epochLastUpdate = IGearStakingV3(_gearStaking).getCurrentEpoch(); // U:[GA-01] epochFrozen = true; // U:[GA-01] + emit SetFrozenEpoch(true); // U:[GA-01] } /// @dev Ensures that function caller is voter diff --git a/contracts/governance/GearStakingV3.sol b/contracts/governance/GearStakingV3.sol index 9d2f2d8c..d46dca60 100644 --- a/contracts/governance/GearStakingV3.sol +++ b/contracts/governance/GearStakingV3.sol @@ -144,7 +144,6 @@ contract GearStakingV3 is ACLNonReentrantTrait, IGearStakingV3 { /// @param votesBefore Votes to apply before sending GEAR to the successor contract /// @param votesBefore Sequence of votes to perform in this contract before sending GEAR to the successor /// @param votesAfter Sequence of votes to perform in the successor contract after sending GEAR - /// @custom:expects This contract is set as `migrator` in the `successor` contract, otherwise this would revert function migrate(uint96 amount, MultiVote[] calldata votesBefore, MultiVote[] calldata votesAfter) external override @@ -317,9 +316,13 @@ contract GearStakingV3 is ACLNonReentrantTrait, IGearStakingV3 { /// @notice Sets a new successor contract /// @dev Successor is a new staking contract where staked GEAR can be migrated, bypassing the withdrawal delay. /// This is used to upgrade staking contracts when new functionality is added. + /// It must already have this contract set as migrator. /// @param newSuccessor Address of the new successor contract function setSuccessor(address newSuccessor) external override configuratorOnly { if (successor != newSuccessor) { + if (IGearStakingV3(newSuccessor).migrator() != address(this)) { + revert IncompatibleSuccessorException(); // U: [GS-08] + } successor = newSuccessor; // U: [GS-08] emit SetSuccessor(newSuccessor); // U: [GS-08] diff --git a/contracts/governance/PolicyManagerV3.sol b/contracts/governance/PolicyManagerV3.sol index 7e774fdb..d31b899f 100644 --- a/contracts/governance/PolicyManagerV3.sol +++ b/contracts/governance/PolicyManagerV3.sol @@ -205,6 +205,7 @@ abstract contract PolicyManagerV3 is ACLNonReentrantTrait { return true; } + /// @dev Returns the absolute difference between two numbers and the flag whether the first one is greater function calcDiff(uint256 a, uint256 b) internal pure returns (uint256, bool) { return a > b ? (a - b, true) : (b - a, false); } diff --git a/contracts/interfaces/IAddressProviderV3.sol b/contracts/interfaces/IAddressProviderV3.sol index cdc61955..0bdbe5c1 100644 --- a/contracts/interfaces/IAddressProviderV3.sol +++ b/contracts/interfaces/IAddressProviderV3.sol @@ -16,7 +16,6 @@ bytes32 constant AP_TREASURY = "TREASURY"; bytes32 constant AP_GEAR_TOKEN = "GEAR_TOKEN"; bytes32 constant AP_WETH_TOKEN = "WETH_TOKEN"; bytes32 constant AP_WETH_GATEWAY = "WETH_GATEWAY"; -bytes32 constant AP_WITHDRAWAL_MANAGER = "WITHDRAWAL_MANAGER"; bytes32 constant AP_ROUTER = "ROUTER"; bytes32 constant AP_BOT_LIST = "BOT_LIST"; bytes32 constant AP_GEAR_STAKING = "GEAR_STAKING"; diff --git a/contracts/interfaces/IBotListV3.sol b/contracts/interfaces/IBotListV3.sol index 6c7a33d7..0a13b9ae 100644 --- a/contracts/interfaces/IBotListV3.sol +++ b/contracts/interfaces/IBotListV3.sol @@ -5,139 +5,80 @@ pragma solidity ^0.8.17; import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; -struct BotFunding { - uint72 totalFundingAllowance; - uint72 maxWeeklyAllowance; - uint72 remainingWeeklyAllowance; - uint40 allowanceLU; -} - -struct BotSpecialStatus { +/// @notice Bot info +/// @param forbidden Whether bot is forbidden +/// @param specialPermissions Mapping credit manager => bot's special permissions +/// @param permissions Mapping credit manager => credit account => bot's permissions +struct BotInfo { bool forbidden; - uint192 specialPermissions; + mapping(address => uint192) specialPermissions; + mapping(address => mapping(address => uint192)) permissions; } interface IBotListV3Events { - /// @notice Emitted when credit account owner changes bot permissions and/or funding parameters + // ----------- // + // PERMISSIONS // + // ----------- // + + /// @notice Emitted when new `bot`'s permissions and funding params are set for `creditAccount` in `creditManager` event SetBotPermissions( - address indexed creditManager, - address indexed creditAccount, - address indexed bot, - uint256 permissions, - uint72 totalFundingAllowance, - uint72 weeklyFundingAllowance + address indexed bot, address indexed creditManager, address indexed creditAccount, uint192 permissions ); - /// @notice Emitted when a bot is forbidden in a Credit Manager - event SetBotForbiddenStatus(address indexed creditManager, address indexed bot, bool status); - - /// @notice Emitted when a bot is granted special permissions in a Credit Manager - event SetBotSpecialPermissions(address indexed creditManager, address indexed bot, uint192 permissions); - - /// @notice Emitted when the user deposits funds to their bot wallet - event Deposit(address indexed payer, uint256 amount); - - /// @notice Emitted when the user withdraws funds from their bot wallet - event Withdraw(address indexed payer, uint256 amount); + /// @notice Emitted when `bot`'s permissions and funding params are removed for `creditAccount` in `creditManager` + event EraseBot(address indexed bot, address indexed creditManager, address indexed creditAccount); - /// @notice Emitted when the bot is paid for performed services - event PayBot( - address indexed payer, - address indexed creditAccount, - address indexed bot, - uint72 paymentAmount, - uint72 daoFeeAmount - ); + // ------------- // + // CONFIGURATION // + // ------------- // - /// @notice Emitted when the DAO sets a new fee on bot payments - event SetBotDAOFee(uint16 newFee); + /// @notice Emitted when `bot`'s forbidden status is set + event SetBotForbiddenStatus(address indexed bot, bool forbidden); - /// @notice Emitted when all bot permissions for a Credit Account are erased - event EraseBot(address indexed creditManager, address indexed creditAccount, address indexed bot); + /// @notice Emitted when `bot`'s special permissions in `creditManager` are set + event SetBotSpecialPermissions(address indexed bot, address indexed creditManager, uint192 permissions); - /// @notice Emitted when Credit Manager's status in the bot list is changed - event SetCreditManagerStatus(address indexed creditManager, bool newStatus); + /// @notice Emitted when `creditManager`'s approved status is set + event SetCreditManagerApprovedStatus(address indexed creditManager, bool approved); } /// @title Bot list V3 interface interface IBotListV3 is IBotListV3Events, IVersion { - function weth() external view returns (address); - - function treasury() external view returns (address); - // ----------- // // PERMISSIONS // // ----------- // - function setBotPermissions( - address creditManager, - address creditAccount, - address bot, - uint192 permissions, - uint72 totalFundingAllowance, - uint72 weeklyFundingAllowance - ) external returns (uint256 activeBotsRemaining); - - function eraseAllBotPermissions(address creditManager, address creditAccount) external; - - function getActiveBots(address creditManager, address creditAccount) external view returns (address[] memory); - - function botPermissions(address creditManager, address creditAccount, address bot) + function botPermissions(address bot, address creditManager, address creditAccount) external view returns (uint192); - function botFunding(address creditManager, address creditAccount, address bot) - external - view - returns (uint72 remainingFunds, uint72 maxWeeklyAllowance, uint72 remainingWeeklyAllowance, uint40 allowanceLU); + function activeBots(address creditManager, address creditAccount) external view returns (address[] memory); - function getBotStatus(address creditManager, address creditAccount, address bot) + function getBotStatus(address bot, address creditManager, address creditAccount) external view returns (uint192 permissions, bool forbidden, bool hasSpecialPermissions); - // ------- // - // FUNDING // - // ------- // - - function name() external view returns (string memory); - - function symbol() external view returns (string memory); - - function balanceOf(address payer) external view returns (uint256); - - function deposit() external payable; - - function withdraw(uint256 amount) external; + function setBotPermissions(address bot, address creditManager, address creditAccount, uint192 permissions) + external + returns (uint256 activeBotsRemaining); - function payBot(address payer, address creditManager, address creditAccount, address bot, uint72 paymentAmount) - external; + function eraseAllBotPermissions(address creditManager, address creditAccount) external; // ------------- // // CONFIGURATION // // ------------- // - function daoFee() external view returns (uint16); - - function collectedDaoFees() external view returns (uint64); - - function approvedCreditManager(address) external view returns (bool); - - function botSpecialStatus(address creditManager, address bot) - external - view - returns (bool forbidden, uint192 specialPermissions); - - function setBotForbiddenStatus(address creditManager, address bot, bool status) external; + function botForbiddenStatus(address bot) external view returns (bool); - function setBotForbiddenStatusEverywhere(address bot, bool status) external; + function botSpecialPermissions(address bot, address creditManager) external view returns (uint192); - function setBotSpecialPermissions(address creditManager, address bot, uint192 permissions) external; + function approvedCreditManager(address creditManager) external view returns (bool); - function setDAOFee(uint16 newFee) external; + function setBotForbiddenStatus(address bot, bool forbidden) external; - function setApprovedCreditManagerStatus(address creditManager, bool newStatus) external; + function setBotSpecialPermissions(address bot, address creditManager, uint192 permissions) external; - function transferCollectedDaoFees() external; + function setCreditManagerApprovedStatus(address creditManager, bool approved) external; } diff --git a/contracts/interfaces/ICreditFacadeV3.sol b/contracts/interfaces/ICreditFacadeV3.sol index 07b13aa4..7c80c124 100644 --- a/contracts/interfaces/ICreditFacadeV3.sol +++ b/contracts/interfaces/ICreditFacadeV3.sol @@ -6,7 +6,6 @@ pragma solidity ^0.8.17; import {MultiCall} from "@gearbox-protocol/core-v2/contracts/libraries/MultiCall.sol"; import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; -import {ClosureAction} from "../interfaces/ICreditManagerV3.sol"; import "./ICreditFacadeV3Multicall.sol"; import {AllowanceAction} from "../interfaces/ICreditConfiguratorV3.sol"; @@ -31,10 +30,14 @@ struct CumulativeLossParams { /// 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 enabledTokensMaskAfter 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 enabledTokensMaskAfter; + bool revertOnForbiddenTokens; + bool useSafePrices; } interface ICreditFacadeV3Events { @@ -44,7 +47,7 @@ interface ICreditFacadeV3Events { ); /// @notice Emitted when account is closed - event CloseCreditAccount(address indexed creditAccount, address indexed borrower, address indexed to); + event CloseCreditAccount(address indexed creditAccount, address indexed borrower); /// @notice Emitted when account is liquidated event LiquidateCreditAccount( @@ -52,7 +55,6 @@ interface ICreditFacadeV3Events { address indexed borrower, address indexed liquidator, address to, - ClosureAction closureAction, uint256 remainingFunds ); @@ -63,7 +65,10 @@ interface ICreditFacadeV3Events { event DecreaseDebt(address indexed creditAccount, uint256 amount); /// @notice Emitted when collateral is added to account - event AddCollateral(address indexed creditAccount, address indexed token, uint256 value); + event AddCollateral(address indexed creditAccount, address indexed token, uint256 amount); + + /// @notice Emitted when collateral is withdrawn from account + event WithdrawCollateral(address indexed creditAccount, address indexed token, uint256 amount); /// @notice Emitted when a multicall is started event StartMultiCall(address indexed creditAccount, address indexed caller); @@ -73,9 +78,6 @@ interface ICreditFacadeV3Events { /// @notice Emitted when a multicall is finished event FinishMultiCall(); - - /// @notice Emitted when the mask of account's enabled tokens is updated - event SetEnabledTokensMask(address indexed creditAccount, uint256 enabledTokensMask); } /// @title Credit facade V3 interface @@ -88,8 +90,6 @@ interface ICreditFacadeV3 is IVersion, ICreditFacadeV3Events { function botList() external view returns (address); - function withdrawalManager() external view returns (address); - function maxDebtPerBlockMultiplier() external view returns (uint8); function maxQuotaMultiplier() external view returns (uint256); @@ -117,35 +117,15 @@ interface ICreditFacadeV3 is IVersion, ICreditFacadeV3Events { payable returns (address creditAccount); - function closeCreditAccount( - address creditAccount, - address to, - uint256 skipTokenMask, - bool convertToETH, - MultiCall[] calldata calls - ) external payable; + function closeCreditAccount(address creditAccount, MultiCall[] calldata calls) external payable; - function liquidateCreditAccount( - address creditAccount, - address to, - uint256 skipTokenMask, - bool convertToETH, - MultiCall[] calldata calls - ) external; + function liquidateCreditAccount(address creditAccount, address to, MultiCall[] calldata calls) external; function multicall(address creditAccount, MultiCall[] calldata calls) external payable; function botMulticall(address creditAccount, MultiCall[] calldata calls) external; - function claimWithdrawals(address creditAccount, address to) external; - - function setBotPermissions( - address creditAccount, - address bot, - uint192 permissions, - uint72 fundingAmount, - uint72 weeklyFundingAllowance - ) external; + function setBotPermissions(address creditAccount, address bot, uint192 permissions) external; // ------------- // // CONFIGURATION // diff --git a/contracts/interfaces/ICreditFacadeV3Multicall.sol b/contracts/interfaces/ICreditFacadeV3Multicall.sol index d9ab34f6..fc85f19b 100644 --- a/contracts/interfaces/ICreditFacadeV3Multicall.sol +++ b/contracts/interfaces/ICreditFacadeV3Multicall.sol @@ -15,14 +15,14 @@ uint192 constant INCREASE_DEBT_PERMISSION = 1 << 1; uint192 constant DECREASE_DEBT_PERMISSION = 1 << 2; uint192 constant ENABLE_TOKEN_PERMISSION = 1 << 3; uint192 constant DISABLE_TOKEN_PERMISSION = 1 << 4; -uint192 constant WITHDRAW_PERMISSION = 1 << 5; +uint192 constant WITHDRAW_COLLATERAL_PERMISSION = 1 << 5; uint192 constant UPDATE_QUOTA_PERMISSION = 1 << 6; uint192 constant REVOKE_ALLOWANCES_PERMISSION = 1 << 7; uint192 constant EXTERNAL_CALLS_PERMISSION = 1 << 16; -uint256 constant ALL_CREDIT_FACADE_CALLS_PERMISSION = ADD_COLLATERAL_PERMISSION | INCREASE_DEBT_PERMISSION - | DECREASE_DEBT_PERMISSION | ENABLE_TOKEN_PERMISSION | DISABLE_TOKEN_PERMISSION | WITHDRAW_PERMISSION +uint256 constant ALL_CREDIT_FACADE_CALLS_PERMISSION = ADD_COLLATERAL_PERMISSION | WITHDRAW_COLLATERAL_PERMISSION + | INCREASE_DEBT_PERMISSION | DECREASE_DEBT_PERMISSION | ENABLE_TOKEN_PERMISSION | DISABLE_TOKEN_PERMISSION | UPDATE_QUOTA_PERMISSION | REVOKE_ALLOWANCES_PERMISSION; uint256 constant ALL_PERMISSIONS = ALL_CREDIT_FACADE_CALLS_PERMISSION | EXTERNAL_CALLS_PERMISSION; @@ -34,28 +34,21 @@ uint256 constant ALL_PERMISSIONS = ALL_CREDIT_FACADE_CALLS_PERMISSION | EXTERNAL /// @dev Indicates that there are enabled forbidden tokens on the account before multicall uint256 constant FORBIDDEN_TOKENS_BEFORE_CALLS = 1 << 192; -/// @dev Indicates that there must be no enabled forbidden tokens on the account after multicall, -/// set to true when `increaseDebt` or `scheduleWithdrawal` is called -uint256 constant REVERT_ON_FORBIDDEN_TOKENS_AFTER_CALLS = 1 << 193; - /// @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 << 194; - -/// @dev Indicates that `payBot` can be called during multicall to fund the bot, set to true when -/// multicall is initiated in `botMulticall` and reset after the first `payBot` call -uint256 constant PAY_BOT_CAN_BE_CALLED = 1 << 195; +uint256 constant EXTERNAL_CONTRACT_WAS_CALLED = 1 << 193; /// @title Credit facade V3 multicall interface /// @dev Unless specified otherwise, all these methods are only available in `openCreditAccount`, -/// `multicall`, and, with account owner's permission, `botMulticall` +/// `closeCreditAccount`, `multicall`, and, with account owner's permission, `botMulticall` interface ICreditFacadeV3Multicall { /// @notice Updates the price for a token with on-demand updatable price feed /// @param token Token to push the price update for + /// @param reserve Whether to update reserve price feed or main price feed /// @param data Data to call `updatePrice` with /// @dev Calls of this type must be placed before all other calls in the multicall not to revert /// @dev This method is available in all kinds of multicalls - function onDemandPriceUpdate(address token, bytes calldata data) external; + function onDemandPriceUpdate(address token, bool reserve, bytes calldata data) external; /// @notice Ensures that token balances increase at least by specified deltas after the following calls /// @param balanceDeltas Array of (token, minBalanceDelta) pairs, deltas are allowed to be negative @@ -67,6 +60,7 @@ interface ICreditFacadeV3Multicall { /// @param token Token to add /// @param amount Amount to add /// @dev Requires token approval from caller to the credit manager + /// @dev This method can also be called during liquidation function addCollateral(address token, uint256 amount) external; /// @notice Adds collateral to account using signed EIP-2612 permit message @@ -74,11 +68,13 @@ interface ICreditFacadeV3Multicall { /// @param amount Amount to add /// @param deadline Permit deadline /// @dev `v`, `r`, `s` must be a valid signature of the permit message from caller to the credit manager + /// @dev This method can also be called during liquidation function addCollateralWithPermit(address token, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; /// @notice Increases account's debt /// @param amount Underlying amount to borrow + /// @dev Increasing debt is prohibited when closing an account /// @dev Increasing debt is prohibited if it was previously updated in the same block /// @dev The resulting debt amount must be within allowed range /// @dev Increasing debt is prohibited if there are forbidden tokens enabled as collateral on the account @@ -88,6 +84,7 @@ interface ICreditFacadeV3Multicall { /// @notice Decreases account's debt /// @param amount Underlying amount to repay, value above account's total debt indicates full repayment + /// @dev Decreasing debt is prohibited when opening an account /// @dev Decreasing debt is prohibited if it was previously updated in the same block /// @dev The resulting debt amount must be within allowed range or zero /// @dev Full repayment brings account into a special mode that skips collateral checks and thus requires @@ -96,7 +93,7 @@ interface ICreditFacadeV3Multicall { /// @notice Updates account's quota for a token /// @param token Token to update the quota for - /// @param quotaChange Desired quota change in underlying token units + /// @param quotaChange Desired quota change in underlying token units (`type(int96).min` to disable quota) /// @param minQuota Minimum resulting account's quota for token required not to revert /// @dev Enables token as collateral if quota is increased from zero, disables if decreased to zero /// @dev Quota increase is prohibited if there are forbidden tokens enabled as collateral on the account @@ -104,17 +101,15 @@ interface ICreditFacadeV3Multicall { /// @dev Resulting account's quota for token must not exceed the limit defined in the facade function updateQuota(address token, int96 quotaChange, uint96 minQuota) external; - /// @notice Schedules a delayed withdrawal from account + /// @notice Withdraws collateral from account /// @param token Token to withdraw - /// @param amount Amount to withdraw - /// @dev Withdrawals are prohibited if there are forbidden tokens enabled as collateral on the account + /// @param amount Amount to withdraw, `type(uint256).max` to withdraw all balance + /// @param to Token recipient + /// @dev This method can also be called during liquidation + /// @dev Withdrawals are prohibited in multicalls if there are forbidden tokens enabled as collateral on the account /// @dev Withdrawals are prohibited when opening an account - function scheduleWithdrawal(address token, uint256 amount) external; - - /// @notice Requests bot list to make a payment to the caller - /// @param paymentAmount Paymenet amount in WETH - /// @dev This method is only available in `botMulticall` and can only be called once - function payBot(uint72 paymentAmount) external; + /// @dev Withdrawals activate safe pricing (min of main and reserve feeds) in collateral check + function withdrawCollateral(address token, uint256 amount, address to) external; /// @notice Sets advanced collateral check parameters /// @param collateralHints Optional array of token masks to check first to reduce the amount of computation diff --git a/contracts/interfaces/ICreditManagerV3.sol b/contracts/interfaces/ICreditManagerV3.sol index 0dcb1bdc..e2575a2f 100644 --- a/contracts/interfaces/ICreditManagerV3.sol +++ b/contracts/interfaces/ICreditManagerV3.sol @@ -5,30 +5,11 @@ pragma solidity ^0.8.17; import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; -import {ClaimAction} from "./IWithdrawalManagerV3.sol"; +uint8 constant BOT_PERMISSIONS_SET_FLAG = 1; -uint8 constant WITHDRAWAL_FLAG = 1; -uint8 constant BOT_PERMISSIONS_SET_FLAG = 1 << 1; - -uint8 constant DEFAULT_MAX_ENABLED_TOKENS = 12; +uint8 constant DEFAULT_MAX_ENABLED_TOKENS = 4; address constant INACTIVE_CREDIT_ACCOUNT_ADDRESS = address(1); -/// @notice Account closure mode -/// - `CLOSE_ACCOUNT` performs normal account closure: -// * repays debt, interest and fees to the pool, reverts in case of underlying shortfall -/// * transfers remaining tokens on the credit account to an owner-specified address -/// - `LIQUIDATE_ACCOUNT` -/// * computes amounts that should be distributed between pool and account owner and potential losses -/// * repays due funds to the pool, charges liquidagtor in case of underlying shortfall -/// * transfers due funds in underlying to account owner (if any) -/// * transfers remaining tokens on the credit account to a liquidator-specified address -/// - `LIQUIDATE_EXPIRED_ACCOUNT` is same as `LIQUIDATE_ACCOUNT` but with lower liquidation premium and fee -enum ClosureAction { - CLOSE_ACCOUNT, - LIQUIDATE_ACCOUNT, - LIQUIDATE_EXPIRED_ACCOUNT -} - /// @notice Debt management type /// - `INCREASE_DEBT` borrows additional funds from the pool, updates account's debt and cumulative interest index /// - `DECREASE_DEBT` repays debt components (quota interest and fees -> base interest and fees -> debt principal) @@ -43,22 +24,18 @@ enum ManageDebtAction { /// - `GENERIC_PARAMS` returns generic data like account debt and cumulative indexes /// - `DEBT_ONLY` is same as `GENERIC_PARAMS` but includes more detailed debt info, like accrued base/quota /// interest and fees -/// - `DEBT_COLLATERAL_WITHOUT_WITHDRAWALS` is same as `DEBT_ONLY` but also returns total value and total -/// LT-weighted value of account's tokens, this mode is used during account closure -/// - `DEBT_COLLATERAL_CANCEL_WITHDRAWALS` is same as `DEBT_COLLATERAL_WITHOUT_WITHDRAWALS` but adds the value -/// of immature scheduled withdrawals to the total value, this mode is used during liquidations -/// - `DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS` is same as `DEBT_COLLATERAL_WITHOUT_WITHDRAWALS` but adds the -/// value of all scheduled withdrawals to the total value, this mode is used during emergency liquidations /// - `FULL_COLLATERAL_CHECK_LAZY` checks whether account is sufficiently collateralized in a lazy fashion, /// i.e. it stops iterating over collateral tokens once TWV reaches the desired target. /// Since it may return underestimated TWV, it's only available for internal use. +/// - `DEBT_COLLATERAL` is same as `DEBT_ONLY` but also returns total value and total LT-weighted value of +/// account's tokens, this mode is used during account liquidation +/// - `DEBT_COLLATERAL_SAFE_PRICES` is same as `DEBT_COLLATERAL` but uses safe prices from price oracle enum CollateralCalcTask { GENERIC_PARAMS, DEBT_ONLY, - DEBT_COLLATERAL_WITHOUT_WITHDRAWALS, - DEBT_COLLATERAL_CANCEL_WITHDRAWALS, - DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS, - FULL_COLLATERAL_CHECK_LAZY + FULL_COLLATERAL_CHECK_LAZY, + DEBT_COLLATERAL, + DEBT_COLLATERAL_SAFE_PRICES } struct CreditAccountInfo { @@ -131,14 +108,13 @@ interface ICreditManagerV3 is IVersion, ICreditManagerV3Events { function openCreditAccount(address onBehalfOf) external returns (address); - function closeCreditAccount( + function closeCreditAccount(address creditAccount) external; + + function liquidateCreditAccount( address creditAccount, - ClosureAction closureAction, CollateralDebtData calldata collateralDebtData, - address payer, address to, - uint256 skipTokensMask, - bool convertToETH + bool isExpired ) external returns (uint256 remainingFunds, uint256 loss); function manageDebt(address creditAccount, uint256 amount, uint256 enabledTokensMask, ManageDebtAction action) @@ -147,7 +123,17 @@ interface ICreditManagerV3 is IVersion, ICreditManagerV3Events { function addCollateral(address payer, address creditAccount, address token, uint256 amount) external - returns (uint256 tokenMask); + returns (uint256 tokensToEnable); + + function withdrawCollateral(address creditAccount, address token, uint256 amount, address to) + external + returns (uint256 tokensToDisable); + + function externalCall(address creditAccount, address target, bytes calldata callData) + external + returns (bytes memory result); + + function approveToken(address creditAccount, address token, address spender, uint256 amount) external; function revokeAdapterAllowances(address creditAccount, RevocationPair[] calldata revocations) external; @@ -177,7 +163,8 @@ interface ICreditManagerV3 is IVersion, ICreditManagerV3Events { address creditAccount, uint256 enabledTokensMask, uint256[] calldata collateralHints, - uint16 minHealthFactor + uint16 minHealthFactor, + bool useSafePrices ) external returns (uint256 enabledTokensMaskAfter); function isLiquidatable(address creditAccount, uint16 minHealthFactor) external view returns (bool); @@ -199,20 +186,6 @@ interface ICreditManagerV3 is IVersion, ICreditManagerV3Events { external returns (uint256 tokensToEnable, uint256 tokensToDisable); - // ----------- // - // WITHDRAWALS // - // ----------- // - - function withdrawalManager() external view returns (address); - - function scheduleWithdrawal(address creditAccount, address token, uint256 amount) - external - returns (uint256 tokensToDisable); - - function claimWithdrawals(address creditAccount, address to, ClaimAction action) - external - returns (uint256 tokensToEnable); - // --------------------- // // CREDIT MANAGER PARAMS // // --------------------- // diff --git a/contracts/interfaces/IExceptions.sol b/contracts/interfaces/IExceptions.sol index d452a846..00fd97a7 100644 --- a/contracts/interfaces/IExceptions.sol +++ b/contracts/interfaces/IExceptions.sol @@ -16,6 +16,9 @@ error AmountCantBeZeroException(); /// @notice Thrown on incorrect input parameter error IncorrectParameterException(); +/// @notice Thrown when balance is insufficient to perform an operation +error InsufficientBalanceException(); + /// @notice Thrown if parameter is out of range error ValueOutOfRangeException(); @@ -64,9 +67,15 @@ error AddressNotFoundException(); /// @notice Thrown by pool-adjacent contracts when a credit manager being connected has a wrong pool address error IncompatibleCreditManagerException(); +/// @notice Thrown when attempting to set an incompatible successor staking contract +error IncompatibleSuccessorException(); + /// @notice Thrown when attempting to vote in a non-approved contract error VotingContractNotAllowedException(); +/// @notice Thrown when attempting to unvote more votes than there are +error InsufficientVotesException(); + /// @notice Thrown when attempting to borrow more than the second point on a two-point curve error BorrowingMoreThanU2ForbiddenException(); @@ -101,6 +110,9 @@ error TooManyEnabledTokensException(); /// @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(); + /// @notice Thrown when attempting to execute a protocol interaction without active credit account set error ActiveCreditAccountNotSetException(); @@ -113,6 +125,12 @@ error DebtToZeroWithActiveQuotasException(); /// @notice Thrown when a zero-debt account attempts to increase quota error IncreaseQuotaOnZeroDebtAccountException(); +/// @notice Thrown when attempting to close an account with non-zero debt +error CloseAccountWithNonZeroDebtException(); + +/// @notice Thrown when value of funds remaining on the account after liquidation is insufficient +error InsufficientRemainingFundsException(); + /// @notice Thrown when Credit Facade tries to write over a non-zero active Credit Account error ActiveCreditAccountOverridenException(); @@ -154,6 +172,9 @@ 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(); +/// @notice Thrown when trying to close an account with enabled tokens +error CloseAccountWithEnabledTokensException(); + /// @notice Thrown if a liquidator tries to liquidate an account with a health factor above 1 error CreditAccountNotLiquidatableException(); @@ -175,6 +196,15 @@ error ExpectedBalancesAlreadySetException(); /// @notice Thrown when trying to perform an action that is forbidden when credit account has enabled forbidden tokens error ForbiddenTokensException(); +/// @notice Thrown when new forbidden tokens are enabled during the multicall +error ForbiddenTokenEnabledException(); + +/// @notice Thrown when enabled forbidden token balance is increased during the multicall +error ForbiddenTokenBalanceIncreasedException(); + +/// @notice Thrown when the remaining token balance is increased during the liquidation +error RemainingTokenBalanceIncreasedException(); + /// @notice Thrown if `botMulticall` is called by an address that is not approved by account owner or is forbidden error NotApprovedBotException(); @@ -184,6 +214,9 @@ error NoPermissionException(uint256 permission); /// @notice Thrown when user tries to approve more bots than allowed error TooManyApprovedBotsException(); +/// @notice Thrown when attempting to give a bot unexpected permissions +error UnexpectedPermissionsException(); + // ------ // // ACCESS // // ------ // @@ -256,25 +289,9 @@ error ParameterChangedAfterQueuedTxException(); // BOT LIST // // -------- // -/// @notice Thrown when attempting to set non-zero permissions for a forbidden bot +/// @notice Thrown when attempting to set non-zero permissions for a forbidden or special bot error InvalidBotException(); -/// @notice Thrown if payment amount bigger that remaining weekly allowance -error InsufficientWeeklyFundingAllowance(); - -/// @notice Thrown if payment amount bigger that remaining total allowance -error InsufficientTotalFundingAllowance(); - -// ------------------ // -// WITHDRAWAL MANAGER // -// ------------------ // - -/// @notice Thrown when attempting to claim funds without having anything claimable -error NothingToClaimException(); - -/// @notice Thrown when attempting to schedule withdrawal from a credit account that has no free withdrawal slots -error NoFreeWithdrawalSlotsException(); - // --------------- // // ACCOUNT FACTORY // // --------------- // @@ -300,13 +317,3 @@ error IncorrectPriceException(); /// @notice Thrown when token's price feed becomes stale error StalePriceException(); - -// --------- // -// DEGEN NFT // -// --------- // - -/// @notice Thrown by Degen NFT when attempting to burn on opening an account with 0 balance -error InsufficientBalanceException(); - -/// @notice Thrown by Degen NFT when attempting to burn on opening an account with 0 balance -error InsufficientVotesException(); diff --git a/contracts/interfaces/IPriceOracleV3.sol b/contracts/interfaces/IPriceOracleV3.sol index 12fc8dfa..f967b15a 100644 --- a/contracts/interfaces/IPriceOracleV3.sol +++ b/contracts/interfaces/IPriceOracleV3.sol @@ -11,11 +11,14 @@ struct PriceFeedParams { bool skipCheck; uint8 decimals; bool useReserve; + bool trusted; } interface IPriceOracleV3Events { /// @notice Emitted when new price feed is set for token - event SetPriceFeed(address indexed token, address indexed priceFeed, uint32 stalenessPeriod, bool skipCheck); + event SetPriceFeed( + address indexed token, address indexed priceFeed, uint32 stalenessPeriod, bool skipCheck, bool trusted + ); /// @notice Emitted when new reserve price feed is set for token event SetReservePriceFeed(address indexed token, address indexed priceFeed, uint32 stalenessPeriod, bool skipCheck); @@ -26,6 +29,8 @@ interface IPriceOracleV3Events { /// @title Price oracle V3 interface interface IPriceOracleV3 is IPriceOracleBase, IPriceOracleV3Events { + function getPriceSafe(address token) external view returns (uint256); + function getPriceRaw(address token, bool reserve) external view returns (uint256); function priceFeedsRaw(address token, bool reserve) external view returns (address); @@ -33,13 +38,15 @@ interface IPriceOracleV3 is IPriceOracleBase, IPriceOracleV3Events { function priceFeedParams(address token) external view - returns (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals); + returns (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals, bool trusted); + + function safeConvertToUSD(uint256 amount, address token) external view returns (uint256); // ------------- // // CONFIGURATION // // ------------- // - function setPriceFeed(address token, address priceFeed, uint32 stalenessPeriod) external; + function setPriceFeed(address token, address priceFeed, uint32 stalenessPeriod, bool trusted) external; function setReservePriceFeed(address token, address priceFeed, uint32 stalenessPeriod) external; diff --git a/contracts/interfaces/IWithdrawalManagerV3.sol b/contracts/interfaces/IWithdrawalManagerV3.sol deleted file mode 100644 index 7a635240..00000000 --- a/contracts/interfaces/IWithdrawalManagerV3.sol +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: MIT -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Foundation, 2023. -pragma solidity ^0.8.17; - -import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; - -/// @notice Withdrawal claim type -/// - `CLAIM` only claims mature withdrawals to specified address -/// - `CANCEL` also claims mature withdrawals but cancels immature ones -/// - `FORCE_CLAIM` claims both mature and immature withdrawals -/// - `FORCE_CANCEL` cancels both mature and immature withdrawals -enum ClaimAction { - CLAIM, - CANCEL, - FORCE_CLAIM, - FORCE_CANCEL -} - -/// @notice Scheduled withdrawal data -/// @param tokenIndex Collateral index of withdrawn token in account's credit manager -/// @param maturity Timestamp after which withdrawal can be claimed -/// @param token Token to withdraw -/// @param amount Amount to withdraw -struct ScheduledWithdrawal { - uint8 tokenIndex; - uint40 maturity; - address token; - uint256 amount; -} - -/// @dev Special address that denotes pure ETH -address constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - -interface IWithdrawalManagerV3Events { - /// @notice Emitted when new immediate withdrawal is added - /// @param account Account immediate withdrawal was added for - /// @param token Token to withdraw - /// @param amount Amount to withdraw - event AddImmediateWithdrawal(address indexed account, address indexed token, uint256 amount); - - /// @notice Emitted when immediate withdrawal is claimed - /// @param account Account that claimed tokens - /// @param token Token claimed - /// @param to Token recipient - /// @param amount Amount claimed - event ClaimImmediateWithdrawal(address indexed account, address indexed token, address to, uint256 amount); - - /// @notice Emitted when new scheduled withdrawal is added - /// @param creditAccount Account to withdraw from - /// @param token Token to withdraw - /// @param amount Amount to withdraw - /// @param maturity Timestamp after which withdrawal can be claimed - event AddScheduledWithdrawal(address indexed creditAccount, address indexed token, uint256 amount, uint40 maturity); - - /// @notice Emitted when scheduled withdrawal is cancelled - /// @param creditAccount Account the token is returned to - /// @param token Token returned - /// @param amount Amount returned - event CancelScheduledWithdrawal(address indexed creditAccount, address indexed token, uint256 amount); - - /// @notice Emitted when scheduled withdrawal is claimed - /// @param creditAccount Account withdrawal was made from - /// @param token Token claimed - /// @param to Token recipient - /// @param amount Amount claimed - event ClaimScheduledWithdrawal(address indexed creditAccount, address indexed token, address to, uint256 amount); - - /// @notice Emitted when new scheduled withdrawal delay is set by configurator - /// @param newDelay New delay for scheduled withdrawals - event SetWithdrawalDelay(uint40 newDelay); - - /// @notice Emitted when new credit manager is added - /// @param creditManager Added credit manager - event AddCreditManager(address indexed creditManager); -} - -/// @title Withdrawal manager interface -interface IWithdrawalManagerV3 is IWithdrawalManagerV3Events, IVersion { - // --------------------- // - // IMMEDIATE WITHDRAWALS // - // --------------------- // - - function weth() external view returns (address); - - function immediateWithdrawals(address account, address token) external view returns (uint256); - - function addImmediateWithdrawal(address token, address to, uint256 amount) external; - - function claimImmediateWithdrawal(address token, address to) external; - - // --------------------- // - // SCHEDULED WITHDRAWALS // - // --------------------- // - - function isValidCreditManager(address addr) external view returns (bool); - - function delay() external view returns (uint40); - - function scheduledWithdrawals(address creditAccount) - external - view - returns (ScheduledWithdrawal[2] memory withdrawals); - - function addScheduledWithdrawal(address creditAccount, address token, uint256 amount, uint8 tokenIndex) external; - - function claimScheduledWithdrawals(address creditAccount, address to, ClaimAction action) - external - returns (bool hasScheduled, uint256 tokensToEnable); - - function cancellableScheduledWithdrawals(address creditAccount, bool isForceCancel) - external - view - returns (address token1, uint256 amount1, address token2, uint256 amount2); - - // ------------- // - // CONFIGURATION // - // ------------- // - - function setWithdrawalDelay(uint40 newDelay) external; - - function addCreditManager(address newCreditManager) external; -} diff --git a/contracts/libraries/BalancesLogic.sol b/contracts/libraries/BalancesLogic.sol index ca98c5e0..5f815334 100644 --- a/contracts/libraries/BalancesLogic.sol +++ b/contracts/libraries/BalancesLogic.sol @@ -22,112 +22,127 @@ struct BalanceDelta { int256 amount; } +enum Comparison { + GREATER, + LESS +} + /// @title Balances logic library /// @notice Implements functions for before-and-after balance comparisons library BalancesLogic { using BitMask for uint256; using SafeCast for int256; + using SafeCast for uint256; using SafeERC20 for IERC20; + /// @dev Compares current `token` balance with `value` + /// @param token Token to check balance for + /// @param value Value to compare current token balance with + /// @param comparison Whether current balance must be greater/less than or equal to `value` + function checkBalance(address creditAccount, address token, uint256 value, Comparison comparison) + internal + view + returns (bool) + { + uint256 current = IERC20(token).safeBalanceOf(creditAccount); + return (comparison == Comparison.GREATER && current >= value) + || (comparison == Comparison.LESS && current <= value); // U:[BLL-1] + } + /// @dev Returns an array of expected token balances after operations - /// @param creditAccount Credit account to compute expected balances for - /// @param deltas The array of (token, amount) structs that contain expected balance increases + /// @param creditAccount Credit account to compute balances for + /// @param deltas Array of expected token balance changes function storeBalances(address creditAccount, BalanceDelta[] memory deltas) internal view - returns (Balance[] memory expected) + returns (Balance[] memory balances) { uint256 len = deltas.length; - expected = new Balance[](len); // U:[BLL-1] + balances = new Balance[](len); // U:[BLL-2] for (uint256 i = 0; i < len;) { - uint256 balance = IERC20(deltas[i].token).safeBalanceOf({account: creditAccount}); - expected[i] = Balance({token: deltas[i].token, balance: (int256(balance) + deltas[i].amount).toUint256()}); // U:[BLL-1] + int256 balance = IERC20(deltas[i].token).safeBalanceOf(creditAccount).toInt256(); + balances[i] = Balance({token: deltas[i].token, balance: (balance + deltas[i].amount).toUint256()}); // U:[BLL-2] unchecked { ++i; } } } - /// @dev Compares current balances to previously saved expected balances + /// @dev Compares current balances with the previously stored ones /// @param creditAccount Credit account to compare balances for - /// @param expected Expected balances after all operations (from `storeBalances`) - /// @return success False if at least one balance is lower than expected, true otherwise - function compareBalances(address creditAccount, Balance[] memory expected) internal view returns (bool success) { - uint256 len = expected.length; + /// @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 + function compareBalances(address creditAccount, Balance[] memory balances, Comparison comparison) + internal + view + returns (bool success) + { + uint256 len = balances.length; unchecked { for (uint256 i = 0; i < len; ++i) { - if (IERC20(expected[i].token).safeBalanceOf({account: creditAccount}) < expected[i].balance) { - return false; // U:[BLL-2] + if (!BalancesLogic.checkBalance(creditAccount, balances[i].token, balances[i].balance, comparison)) { + return false; // U:[BLL-3] } } } - return true; // U:[BLL-2] + return true; // U:[BLL-3] } - /// @dev Returns balances of enabled forbidden tokens on the credit account + /// @dev Returns balances of specified tokens on the credit account /// @param creditAccount Credit account to compute balances for - /// @param enabledTokensMask Current mask of enabled tokens on the credit account - /// @param forbiddenTokenMask Mask of forbidden tokens in the credit facade - /// @param getTokenByMaskFn A function that returns a token's address by its mask - function storeForbiddenBalances( + /// @param tokensMask Bit mask of tokens to compute balances for + /// @param getTokenByMaskFn Function that returns token's address by its mask + function storeBalances( address creditAccount, - uint256 enabledTokensMask, - uint256 forbiddenTokenMask, + uint256 tokensMask, function (uint256) view returns (address) getTokenByMaskFn - ) internal view returns (BalanceWithMask[] memory forbiddenBalances) { - uint256 forbiddenTokensOnAccount = enabledTokensMask & forbiddenTokenMask; // U:[BLL-3] + ) internal view returns (BalanceWithMask[] memory balances) { + if (tokensMask == 0) return balances; - if (forbiddenTokensOnAccount != 0) { - uint256 i = 0; - forbiddenBalances = new BalanceWithMask[](forbiddenTokensOnAccount.calcEnabledTokens()); - unchecked { - while (forbiddenTokensOnAccount != 0) { - uint256 tokenMask = forbiddenTokensOnAccount & uint256(-int256(forbiddenTokensOnAccount)); - forbiddenTokensOnAccount &= forbiddenTokensOnAccount - 1; + balances = new BalanceWithMask[](tokensMask.calcEnabledTokens()); // U:[BLL-4] + unchecked { + uint256 i; + while (tokensMask != 0) { + uint256 tokenMask = tokensMask & uint256(-int256(tokensMask)); + tokensMask ^= tokenMask; - address token = getTokenByMaskFn(tokenMask); - forbiddenBalances[i].token = token; // U:[BLL-3] - forbiddenBalances[i].tokenMask = tokenMask; // U:[BLL-3] - forbiddenBalances[i].balance = IERC20(token).safeBalanceOf({account: creditAccount}); // U:[BLL-3] - ++i; - } + address token = getTokenByMaskFn(tokenMask); + balances[i] = BalanceWithMask({ + token: token, + tokenMask: tokenMask, + balance: IERC20(token).safeBalanceOf(creditAccount) + }); // U:[BLL-4] + ++i; } } } - /// @dev Compares current balances of forbidden tokens to previously saved + /// @dev Compares current balances of specified tokens with the previously stored ones /// @param creditAccount Credit account to compare balances for - /// @param enabledTokensMaskBefore Mask of enabled tokens on the account before operations - /// @param enabledTokensMaskAfter Mask of enabled tokens on the account after operations - /// @param forbiddenBalances Balances of forbidden tokens before operations (from `storeForbiddenBalances`) - /// @param forbiddenTokenMask Mask of forbidden tokens in the credit facade - /// @return success False if balance of at least one forbidden token increased, true otherwise - function checkForbiddenBalances( + /// @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 + function compareBalances( address creditAccount, - uint256 enabledTokensMaskBefore, - uint256 enabledTokensMaskAfter, - BalanceWithMask[] memory forbiddenBalances, - uint256 forbiddenTokenMask - ) internal view returns (bool success) { - uint256 forbiddenTokensOnAccount = enabledTokensMaskAfter & forbiddenTokenMask; - if (forbiddenTokensOnAccount == 0) return true; // U:[BLL-4] + uint256 tokensMask, + BalanceWithMask[] memory balances, + Comparison comparison + ) internal view returns (bool) { + if (tokensMask == 0) return true; - // Ensure that no new forbidden tokens were enabled - uint256 forbiddenTokensOnAccountBefore = enabledTokensMaskBefore & forbiddenTokenMask; - if (forbiddenTokensOnAccount & ~forbiddenTokensOnAccountBefore != 0) return false; // U:[BLL-4] - - // Then, check that any remaining forbidden tokens didn't have their balances increased unchecked { - uint256 len = forbiddenBalances.length; - for (uint256 i = 0; i < len; ++i) { - if (forbiddenTokensOnAccount & forbiddenBalances[i].tokenMask != 0) { - uint256 currentBalance = IERC20(forbiddenBalances[i].token).safeBalanceOf({account: creditAccount}); - if (currentBalance > forbiddenBalances[i].balance) { - return false; // U:[BLL-4] + 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] } } } } - return true; // U:[BLL-4] + return true; // U:[BLL-5] } } diff --git a/contracts/libraries/CreditLogic.sol b/contracts/libraries/CreditLogic.sol index de23b387..0555548b 100644 --- a/contracts/libraries/CreditLogic.sol +++ b/contracts/libraries/CreditLogic.sol @@ -33,6 +33,7 @@ library CreditLogic { pure returns (uint256) { + if (amount == 0) return 0; return (amount * cumulativeIndexNow) / cumulativeIndexLastUpdate - amount; // U:[CL-1] } @@ -42,23 +43,9 @@ library CreditLogic { return collateralDebtData.debt + collateralDebtData.accruedInterest + collateralDebtData.accruedFees; } - // --------------- // - // ACCOUNT CLOSURE // - // --------------- // - - /// @dev Computes the amount of underlying tokens to send to the pool on credit account closure - /// @param collateralDebtData See `CollateralDebtData` (must have debt data filled) - /// @param amountWithFeeFn Function that, given the exact amount of underlying tokens to receive, - /// returns the amount that needs to be sent - /// @return amountToPool Amount of underlying tokens to send to the pool - /// @return profit Amount of underlying tokens received as fees by the DAO - function calcClosePayments( - CollateralDebtData memory collateralDebtData, - function (uint256) view returns (uint256) amountWithFeeFn - ) internal view returns (uint256 amountToPool, uint256 profit) { - amountToPool = amountWithFeeFn(calcTotalDebt(collateralDebtData)); - profit = collateralDebtData.accruedFees; - } + // ----------- // + // LIQUIDATION // + // ----------- // /// @dev Computes the amount of underlying tokens to send to the pool on credit account liquidation /// - First, liquidation premium and fee are subtracted from account's total value @@ -97,7 +84,7 @@ library CreditLogic { uint256 amountToPoolWithFee = amountWithFeeFn(amountToPool); unchecked { if (totalFunds > amountToPoolWithFee) { - remainingFunds = totalFunds - amountToPoolWithFee - 1; // U:[CL-4] + remainingFunds = totalFunds - amountToPoolWithFee; // U:[CL-4] } else { amountToPoolWithFee = totalFunds; amountToPool = amountMinusFeeFn(totalFunds); // U:[CL-4] @@ -169,6 +156,7 @@ library CreditLogic { pure returns (uint256 newDebt, uint256 newCumulativeIndex) { + if (debt == 0) return (amount, cumulativeIndexNow); newDebt = debt + amount; // U:[CL-2] newCumulativeIndex = ( (cumulativeIndexNow * newDebt * INDEX_PRECISION) diff --git a/contracts/libraries/WithdrawalsLogic.sol b/contracts/libraries/WithdrawalsLogic.sol deleted file mode 100644 index 80c9b8f9..00000000 --- a/contracts/libraries/WithdrawalsLogic.sol +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Foundation, 2023. -pragma solidity ^0.8.17; - -import {ClaimAction, ScheduledWithdrawal} from "../interfaces/IWithdrawalManagerV3.sol"; - -/// @title Withdrawals logic library -library WithdrawalsLogic { - /// @dev Clears withdrawal in storage - function clear(ScheduledWithdrawal storage w) internal { - w.maturity = 1; // U:[WL-1] - w.amount = 1; // U:[WL-1] - } - - /// @dev Returns withdrawn token, its mask in credit manager and withdrawn amount - /// It's expected that the function is only called for scheduled withdrawals - function tokenMaskAndAmount(ScheduledWithdrawal storage w) - internal - view - returns (address token, uint256 mask, uint256 amount) - { - uint256 amount_ = w.amount; - if (amount_ > 1) { - unchecked { - token = w.token; // U:[WL-2] - mask = 1 << w.tokenIndex; // U:[WL-2] - amount = amount_ - 1; // U:[WL-2] - } - } - } - - /// @dev Returns flag indicating whether there are free withdrawal slots and the index of first such slot - function findFreeSlot(ScheduledWithdrawal[2] storage ws) internal view returns (bool found, uint8 slot) { - if (ws[0].maturity <= 1) { - found = true; // U:[WL-3] - } else if (ws[1].maturity <= 1) { - found = true; // U:[WL-3] - slot = 1; // U:[WL-3] - } - } - - /// @dev Returns true if withdrawal with given maturity can be claimed under given action - function claimAllowed(ClaimAction action, uint40 maturity) internal view returns (bool) { - if (maturity <= 1) return false; // U:[WL-4] - if (action == ClaimAction.FORCE_CANCEL) return false; // U:[WL-4] - if (action == ClaimAction.FORCE_CLAIM) return true; // U:[WL-4] - return block.timestamp >= maturity; // U:[WL-4] - } - - /// @dev Returns true if withdrawal with given maturity can be cancelled under given action - function cancelAllowed(ClaimAction action, uint40 maturity) internal view returns (bool) { - if (maturity <= 1) return false; // U:[WL-5] - if (action == ClaimAction.FORCE_CANCEL) return true; // U:[WL-5] - if (action == ClaimAction.FORCE_CLAIM || action == ClaimAction.CLAIM) return false; // U:[WL-5] - return block.timestamp < maturity; // U:[WL-5] - } -} diff --git a/contracts/pool/PoolQuotaKeeperV3.sol b/contracts/pool/PoolQuotaKeeperV3.sol index 5c800aac..a8e8dcf6 100644 --- a/contracts/pool/PoolQuotaKeeperV3.sol +++ b/contracts/pool/PoolQuotaKeeperV3.sol @@ -152,6 +152,12 @@ contract PoolQuotaKeeperV3 is IPoolQuotaKeeperV3, ACLNonReentrantTrait, Contract tokenQuotaParams.totalQuoted = totalQuoted + uint96(quotaChange); // U:[PQK-15] } else { + if (quotaChange == type(int96).min) { + unchecked { + quotaChange = (quoted == 0) ? int96(0) : -int96(quoted - 1); + } + } + uint96 absoluteChange = uint96(-quotaChange); newQuoted = quoted - absoluteChange; tokenQuotaParams.totalQuoted -= absoluteChange; // U:[PQK-15] diff --git a/contracts/test/gas/credit/CreditFacade.gas.t.sol b/contracts/test/gas/credit/CreditFacade.gas.t.sol index 4e43de77..56e74199 100644 --- a/contracts/test/gas/credit/CreditFacade.gas.t.sol +++ b/contracts/test/gas/credit/CreditFacade.gas.t.sol @@ -647,15 +647,11 @@ contract CreditFacadeGasTest is IntegrationTestHelper { emit log_uint(gasSpent); } - /// @dev G:[FA-14]: closeCreditAccount with underlying only + /// @dev G:[FA-14]: closeCreditAccount with no debt and underlying only function test_G_FA_14_closeCreditAccount_gas_estimate_1() public creditTest { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); MultiCall[] memory calls = MultiCallBuilder.build( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeV3Multicall.increaseDebt, (DAI_ACCOUNT_AMOUNT)) - }), MultiCall({ target: address(creditFacade), callData: abi.encodeCall( @@ -672,19 +668,27 @@ contract CreditFacadeGasTest is IntegrationTestHelper { uint256 gasBefore = gasleft(); vm.prank(USER); - creditFacade.closeCreditAccount(creditAccount, USER, 0, false, new MultiCall[](0)); + creditFacade.closeCreditAccount( + creditAccount, + MultiCallBuilder.build( + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall( + ICreditFacadeV3Multicall.withdrawCollateral, (underlying, type(uint256).max, USER) + ) + }) + ) + ); uint256 gasSpent = gasBefore - gasleft(); - emit log_string(string(abi.encodePacked("Gas spent - closeCreditAccount with underlying only: "))); + emit log_string(string(abi.encodePacked("Gas spent - closeCreditAccount with no debt and underlying only: "))); emit log_uint(gasSpent); } - /// @dev G:[FA-15]: closeCreditAccount with two tokens + /// @dev G:[FA-15]: closeCreditAccount with debt and underlying function test_G_FA_15_closeCreditAccount_gas_estimate_2() public creditTest { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); - tokenTestSuite.mint(Tokens.LINK, USER, LINK_ACCOUNT_AMOUNT); - tokenTestSuite.approve(Tokens.LINK, USER, address(creditManager)); MultiCall[] memory calls = MultiCallBuilder.build( MultiCall({ @@ -696,12 +700,6 @@ contract CreditFacadeGasTest is IntegrationTestHelper { callData: abi.encodeCall( ICreditFacadeV3Multicall.addCollateral, (tokenTestSuite.addressOf(Tokens.DAI), DAI_ACCOUNT_AMOUNT) ) - }), - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall( - ICreditFacadeV3Multicall.addCollateral, (tokenTestSuite.addressOf(Tokens.LINK), LINK_ACCOUNT_AMOUNT) - ) }) ); @@ -713,25 +711,31 @@ contract CreditFacadeGasTest is IntegrationTestHelper { uint256 gasBefore = gasleft(); vm.prank(USER); - creditFacade.closeCreditAccount(creditAccount, USER, 0, false, new MultiCall[](0)); + creditFacade.closeCreditAccount( + creditAccount, + MultiCallBuilder.build( + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall(ICreditFacadeV3Multicall.decreaseDebt, (type(uint256).max)) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall( + ICreditFacadeV3Multicall.withdrawCollateral, (underlying, type(uint256).max, USER) + ) + }) + ) + ); uint256 gasSpent = gasBefore - gasleft(); - emit log_string(string(abi.encodePacked("Gas spent - closeCreditAccount with 2 tokens: "))); + emit log_string(string(abi.encodePacked("Gas spent - closeCreditAccount with debt and underlying: "))); emit log_uint(gasSpent); } - /// @dev G:[FA-16]: closeCreditAccount with 2 tokens and active quota interest + /// @dev G:[FA-16]: closeCreditAccount with debt and two tokens function test_G_FA_16_closeCreditAccount_gas_estimate_3() public creditTest { - vm.startPrank(CONFIGURATOR); - gauge.addQuotaToken(tokenTestSuite.addressOf(Tokens.LINK), 500, 500); - poolQuotaKeeper.setTokenLimit(tokenTestSuite.addressOf(Tokens.LINK), type(uint96).max); - creditConfigurator.makeTokenQuoted(tokenTestSuite.addressOf(Tokens.LINK)); - - vm.warp(block.timestamp + 7 days); - gauge.updateEpoch(); - vm.stopPrank(); - + tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); tokenTestSuite.mint(Tokens.LINK, USER, LINK_ACCOUNT_AMOUNT); tokenTestSuite.approve(Tokens.LINK, USER, address(creditManager)); @@ -743,14 +747,13 @@ contract CreditFacadeGasTest is IntegrationTestHelper { MultiCall({ target: address(creditFacade), callData: abi.encodeCall( - ICreditFacadeV3Multicall.addCollateral, (tokenTestSuite.addressOf(Tokens.LINK), LINK_ACCOUNT_AMOUNT) + ICreditFacadeV3Multicall.addCollateral, (tokenTestSuite.addressOf(Tokens.DAI), DAI_ACCOUNT_AMOUNT) ) }), MultiCall({ target: address(creditFacade), callData: abi.encodeCall( - ICreditFacadeV3Multicall.updateQuota, - (tokenTestSuite.addressOf(Tokens.LINK), int96(int256(LINK_ACCOUNT_AMOUNT)), 0) + ICreditFacadeV3Multicall.addCollateral, (tokenTestSuite.addressOf(Tokens.LINK), LINK_ACCOUNT_AMOUNT) ) }) ); @@ -760,16 +763,36 @@ contract CreditFacadeGasTest is IntegrationTestHelper { vm.roll(block.number + 1); - vm.warp(block.timestamp + 30 days); + address linkToken = tokenTestSuite.addressOf(Tokens.LINK); uint256 gasBefore = gasleft(); vm.prank(USER); - creditFacade.closeCreditAccount(creditAccount, USER, 0, false, new MultiCall[](0)); + creditFacade.closeCreditAccount( + creditAccount, + MultiCallBuilder.build( + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall(ICreditFacadeV3Multicall.decreaseDebt, (type(uint256).max)) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall( + ICreditFacadeV3Multicall.withdrawCollateral, (underlying, type(uint256).max, USER) + ) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall( + ICreditFacadeV3Multicall.withdrawCollateral, (linkToken, type(uint256).max, USER) + ) + }) + ) + ); uint256 gasSpent = gasBefore - gasleft(); - emit log_string(string(abi.encodePacked("Gas spent - closeCreditAccount with underlying and quoted token: "))); + emit log_string(string(abi.encodePacked("Gas spent - closeCreditAccount with debt and 2 tokens: "))); emit log_uint(gasSpent); } @@ -795,6 +818,8 @@ contract CreditFacadeGasTest is IntegrationTestHelper { vm.roll(block.number + 1); + address linkToken = tokenTestSuite.addressOf(Tokens.LINK); + calls = MultiCallBuilder.build( MultiCall({ target: address(adapterMock), @@ -802,13 +827,25 @@ contract CreditFacadeGasTest is IntegrationTestHelper { AdapterMock.executeSwapSafeApprove, (tokenTestSuite.addressOf(Tokens.LINK), tokenTestSuite.addressOf(Tokens.DAI), "", true) ) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall(ICreditFacadeV3Multicall.decreaseDebt, (type(uint256).max)) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall(ICreditFacadeV3Multicall.withdrawCollateral, (linkToken, type(uint256).max, USER)) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall(ICreditFacadeV3Multicall.withdrawCollateral, (underlying, type(uint256).max, USER)) }) ); uint256 gasBefore = gasleft(); vm.prank(USER); - creditFacade.closeCreditAccount(creditAccount, USER, 0, false, calls); + creditFacade.closeCreditAccount(creditAccount, calls); uint256 gasSpent = gasBefore - gasleft(); @@ -843,7 +880,7 @@ contract CreditFacadeGasTest is IntegrationTestHelper { uint256 gasBefore = gasleft(); vm.prank(FRIEND); - creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 0, false, new MultiCall[](0)); + creditFacade.liquidateCreditAccount(creditAccount, FRIEND, new MultiCall[](0)); uint256 gasSpent = gasBefore - gasleft(); @@ -889,7 +926,7 @@ contract CreditFacadeGasTest is IntegrationTestHelper { uint256 gasBefore = gasleft(); vm.prank(FRIEND); - creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 0, false, new MultiCall[](0)); + creditFacade.liquidateCreditAccount(creditAccount, FRIEND, new MultiCall[](0)); uint256 gasSpent = gasBefore - gasleft(); @@ -945,8 +982,19 @@ contract CreditFacadeGasTest is IntegrationTestHelper { uint256 gasBefore = gasleft(); - vm.prank(FRIEND); - creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 0, false, new MultiCall[](0)); + vm.startPrank(FRIEND); + creditFacade.liquidateCreditAccount( + creditAccount, + FRIEND, + MultiCallBuilder.build( + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall(ICreditFacadeV3Multicall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT * 2)) + }) + ) + ); + + vm.stopPrank(); uint256 gasSpent = gasBefore - gasleft(); diff --git a/contracts/test/helpers/IntegrationTestHelper.sol b/contracts/test/helpers/IntegrationTestHelper.sol index 70697244..e3ea71f3 100644 --- a/contracts/test/helpers/IntegrationTestHelper.sol +++ b/contracts/test/helpers/IntegrationTestHelper.sol @@ -24,7 +24,6 @@ import {ICreditFacadeV3Multicall} from "../../interfaces/ICreditFacadeV3.sol"; import {CreditManagerV3} from "../../credit/CreditManagerV3.sol"; import {IPriceOracleV3} from "../../interfaces/IPriceOracleV3.sol"; -import {IWithdrawalManagerV3} from "../../interfaces/IWithdrawalManagerV3.sol"; import {CreditManagerOpts, CollateralToken} from "../../credit/CreditConfiguratorV3.sol"; import {PoolFactory} from "../suites/PoolFactory.sol"; @@ -59,7 +58,6 @@ contract IntegrationTestHelper is TestHelper, BalanceHelper, ConfigManager { ContractsRegister cr; AccountFactory accountFactory; IPriceOracleV3 priceOracle; - IWithdrawalManagerV3 withdrawalManager; BotListV3 botList; /// POOL & CREDIT MANAGER @@ -295,7 +293,6 @@ contract IntegrationTestHelper is TestHelper, BalanceHelper, ConfigManager { cr = ContractsRegister(addressProvider.getAddressOrRevert(AP_CONTRACTS_REGISTER, NO_VERSION_CONTROL)); accountFactory = AccountFactory(addressProvider.getAddressOrRevert(AP_ACCOUNT_FACTORY, NO_VERSION_CONTROL)); priceOracle = IPriceOracleV3(addressProvider.getAddressOrRevert(AP_PRICE_ORACLE, 3_00)); - withdrawalManager = IWithdrawalManagerV3(addressProvider.getAddressOrRevert(AP_WITHDRAWAL_MANAGER, 3_00)); botList = BotListV3(payable(addressProvider.getAddressOrRevert(AP_BOT_LIST, 3_00))); } @@ -447,7 +444,7 @@ contract IntegrationTestHelper is TestHelper, BalanceHelper, ConfigManager { pool.setCreditManagerDebtLimit(address(creditManager), cmParams.poolLimit); vm.prank(CONFIGURATOR); - botList.setApprovedCreditManagerStatus(address(creditManager), true); + botList.setCreditManagerApprovedStatus(address(creditManager), true); vm.label(address(creditFacade), "CreditFacadeV3"); vm.label(address(creditManager), "CreditManagerV3"); @@ -503,7 +500,7 @@ contract IntegrationTestHelper is TestHelper, BalanceHelper, ConfigManager { vm.startPrank(CONFIGURATOR); creditManager.addToken(address(t)); - IPriceOracleV3(address(priceOracle)).setPriceFeed(address(t), address(pf), 1 hours); + IPriceOracleV3(address(priceOracle)).setPriceFeed(address(t), address(pf), 1 hours, false); creditManager.setCollateralTokenData(address(t), 8000, 8000, type(uint40).max, 0); vm.stopPrank(); diff --git a/contracts/test/integration/credit/Bots.int.sol b/contracts/test/integration/credit/Bots.int.sol index 876566de..de12e9a9 100644 --- a/contracts/test/integration/credit/Bots.int.sol +++ b/contracts/test/integration/credit/Bots.int.sol @@ -12,7 +12,6 @@ import {ICreditAccountBase} from "../../../interfaces/ICreditAccountV3.sol"; import { ICreditManagerV3, ICreditManagerV3Events, - ClosureAction, ManageDebtAction, BOT_PERMISSIONS_SET_FLAG } from "../../../interfaces/ICreditManagerV3.sol"; @@ -57,9 +56,7 @@ contract BotsIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events { bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); vm.prank(address(creditFacade)); - botList.setBotPermissions( - address(creditManager), creditAccount, bot, type(uint192).max, uint72(1 ether), uint72(1 ether / 10) - ); + botList.setBotPermissions(bot, address(creditManager), creditAccount, uint192(ALL_PERMISSIONS)); vm.expectRevert(NotApprovedBotException.selector); creditFacade.botMulticall( @@ -71,14 +68,14 @@ contract BotsIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events { ); vm.prank(CONFIGURATOR); - botList.setBotSpecialPermissions(address(creditManager), address(bot), type(uint192).max); + botList.setBotSpecialPermissions(address(bot), address(creditManager), type(uint192).max); vm.prank(bot); creditFacade.botMulticall(creditAccount, calls); vm.prank(CONFIGURATOR); - botList.setBotSpecialPermissions(address(creditManager), address(bot), 0); + botList.setBotSpecialPermissions(address(bot), address(creditManager), 0); vm.prank(USER); - creditFacade.setBotPermissions(creditAccount, bot, type(uint192).max, uint72(1 ether), uint72(1 ether / 10)); + creditFacade.setBotPermissions(creditAccount, bot, uint192(ALL_PERMISSIONS)); botList.getBotStatus({creditManager: address(creditManager), creditAccount: creditAccount, bot: bot}); @@ -104,7 +101,7 @@ contract BotsIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events { vm.expectCall( address(creditManager), abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, 1, new uint256[](0), PERCENTAGE_FACTOR) + ICreditManagerV3.fullCollateralCheck, (creditAccount, 1, new uint256[](0), PERCENTAGE_FACTOR, false) ) ); @@ -112,7 +109,7 @@ contract BotsIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events { creditFacade.botMulticall(creditAccount, calls); vm.prank(CONFIGURATOR); - botList.setBotForbiddenStatus(address(creditManager), bot, true); + botList.setBotForbiddenStatus(bot, true); vm.expectRevert(NotApprovedBotException.selector); vm.prank(bot); @@ -127,7 +124,7 @@ contract BotsIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events { vm.expectRevert(CallerNotCreditAccountOwnerException.selector); vm.prank(FRIEND); - creditFacade.setBotPermissions(creditAccount, bot, type(uint192).max, uint72(1 ether), uint72(1 ether / 10)); + creditFacade.setBotPermissions(creditAccount, bot, uint192(ALL_PERMISSIONS)); vm.expectCall( address(creditManager), @@ -135,7 +132,7 @@ contract BotsIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events { ); vm.prank(USER); - creditFacade.setBotPermissions(creditAccount, bot, type(uint192).max, uint72(1 ether), uint72(1 ether / 10)); + creditFacade.setBotPermissions(creditAccount, bot, uint192(ALL_PERMISSIONS)); assertTrue(creditManager.flagsOf(creditAccount) & BOT_PERMISSIONS_SET_FLAG > 0, "Flag was not set"); @@ -145,7 +142,7 @@ contract BotsIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events { ); vm.prank(USER); - creditFacade.setBotPermissions(creditAccount, bot, 0, 0, 0); + creditFacade.setBotPermissions(creditAccount, bot, 0); assertTrue(creditManager.flagsOf(creditAccount) & BOT_PERMISSIONS_SET_FLAG == 0, "Flag was not set"); } diff --git a/contracts/test/integration/credit/CloseCreditAccount.int.sol b/contracts/test/integration/credit/CloseCreditAccount.int.sol index 414c1c23..e236f5f5 100644 --- a/contracts/test/integration/credit/CloseCreditAccount.int.sol +++ b/contracts/test/integration/credit/CloseCreditAccount.int.sol @@ -13,7 +13,6 @@ import {SECONDS_PER_YEAR} from "@gearbox-protocol/core-v2/contracts/libraries/Co import { ICreditManagerV3, ICreditManagerV3Events, - ClosureAction, ManageDebtAction, BOT_PERMISSIONS_SET_FLAG } from "../../../interfaces/ICreditManagerV3.sol"; @@ -55,18 +54,15 @@ uint16 constant REFERRAL_CODE = 23; contract CloseCreditAccountIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Events { /// @dev I:[CCA-1]: closeCreditAccount reverts if borrower has no account - function test_I_CCA_01_closeCreditAccount_reverts_if_credit_account_not_exists() public creditTest { + function test_I_CCA_01_closeCreditAccount_reverts_if_credit_account_does_not_exist() public creditTest { vm.expectRevert(CreditAccountDoesNotExistException.selector); vm.prank(USER); - creditFacade.closeCreditAccount(DUMB_ADDRESS, FRIEND, 0, false, MultiCallBuilder.build()); + creditFacade.closeCreditAccount(DUMB_ADDRESS, MultiCallBuilder.build()); vm.expectRevert(CreditAccountDoesNotExistException.selector); vm.prank(USER); creditFacade.closeCreditAccount( DUMB_ADDRESS, - FRIEND, - 0, - false, MultiCallBuilder.build( MultiCall({ target: address(creditFacade), @@ -77,7 +73,7 @@ contract CloseCreditAccountIntegrationTest is IntegrationTestHelper, ICreditFaca vm.expectRevert(CreditAccountDoesNotExistException.selector); vm.prank(USER); - creditFacade.liquidateCreditAccount(DUMB_ADDRESS, DUMB_ADDRESS, 0, false, MultiCallBuilder.build()); + creditFacade.liquidateCreditAccount(DUMB_ADDRESS, DUMB_ADDRESS, MultiCallBuilder.build()); vm.expectRevert(CreditAccountDoesNotExistException.selector); vm.prank(USER); @@ -90,28 +86,76 @@ contract CloseCreditAccountIntegrationTest is IntegrationTestHelper, ICreditFaca }) ) ); + } + + /// @dev I:[CCA-2]: closeCreditAccount reverts if debt is not repaid + function test_I_CCA_02_closeCreditAccount_reverts_if_debt_is_not_repaid() public creditTest { + MultiCall[] memory calls = MultiCallBuilder.build( + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall(ICreditFacadeV3Multicall.increaseDebt, (DAI_ACCOUNT_AMOUNT)) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall( + ICreditFacadeV3Multicall.addCollateral, (tokenTestSuite.addressOf(Tokens.DAI), DAI_ACCOUNT_AMOUNT / 2) + ) + }) + ); - // vm.prank(CONFIGURATOR); - // creditConfigurator.allowContract(address(targetMock), address(adapterMock)); + vm.prank(USER); + address creditAccount = creditFacade.openCreditAccount(USER, calls, 0); + + vm.roll(block.number + 1); + + // debt not repaid at all + vm.expectRevert(CloseAccountWithNonZeroDebtException.selector); + vm.prank(USER); + creditFacade.closeCreditAccount( + creditAccount, + MultiCallBuilder.build( + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall(ICreditFacadeV3Multicall.disableToken, (underlying)) + }) + ) + ); + + // debt partially repaid + vm.expectRevert(CloseAccountWithNonZeroDebtException.selector); + vm.prank(USER); + creditFacade.closeCreditAccount( + creditAccount, + MultiCallBuilder.build( + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall(ICreditFacadeV3Multicall.decreaseDebt, (DAI_ACCOUNT_AMOUNT / 2)) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall(ICreditFacadeV3Multicall.disableToken, (underlying)) + }) + ) + ); } - /// @dev I:[CCA-2]: closeCreditAccount correctly wraps ETH - function test_I_CCA_02_closeCreditAccount_correctly_wraps_ETH() public creditTest { - (address creditAccount,) = _openTestCreditAccount(); + /// @dev I:[CCA-3]: closeCreditAccount correctly wraps ETH + function test_I_CCA_03_closeCreditAccount_correctly_wraps_ETH() public creditTest { + vm.prank(USER); + address creditAccount = creditFacade.openCreditAccount(USER, MultiCallBuilder.build(), 0); vm.roll(block.number + 1); _prepareForWETHTest(); vm.prank(USER); - creditFacade.closeCreditAccount{value: WETH_TEST_AMOUNT}( - creditAccount, USER, 0, false, MultiCallBuilder.build() - ); + creditFacade.closeCreditAccount{value: WETH_TEST_AMOUNT}(creditAccount, MultiCallBuilder.build()); _checkForWETHTest(); } - /// @dev I:[CCA-3]: closeCreditAccount runs multicall operations in correct order - function test_I_CCA_03_closeCreditAccount_runs_operations_in_correct_order() public withAdapterMock creditTest { - (address creditAccount,) = _openTestCreditAccount(); + /// @dev I:[CCA-4]: closeCreditAccount runs operations in correct order + function test_I_CCA_04_closeCreditAccount_runs_operations_in_correct_order() public withAdapterMock creditTest { + vm.prank(USER); + address creditAccount = creditFacade.openCreditAccount(USER, MultiCallBuilder.build(), 0); bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); @@ -125,9 +169,7 @@ contract CloseCreditAccountIntegrationTest is IntegrationTestHelper, ICreditFaca creditFacade.setBotPermissions({ creditAccount: creditAccount, bot: bot, - permissions: uint192(ADD_COLLATERAL_PERMISSION), - totalFundingAllowance: 0, - weeklyFundingAllowance: 0 + permissions: uint192(ADD_COLLATERAL_PERMISSION) }); // LIST OF EXPECTED CALLS @@ -155,49 +197,25 @@ contract CloseCreditAccountIntegrationTest is IntegrationTestHelper, ICreditFaca address(botList), abi.encodeCall(BotListV3.eraseAllBotPermissions, (address(creditManager), creditAccount)) ); - // todo: add withdrawal manager call - - // vm.expectCall( - // address(creditManager), - // abi.encodeCall( - // ICreditManagerV3.closeCreditAccount, - // (creditAccount, ClosureAction.CLOSE_ACCOUNT, 0, USER, FRIEND, 1, 10, DAI_ACCOUNT_AMOUNT, true) - // ) - // ); - vm.expectEmit(true, true, false, false); - emit CloseCreditAccount(creditAccount, USER, FRIEND); + emit CloseCreditAccount(creditAccount, USER); // increase block number, cause it's forbidden to close ca in the same block vm.roll(block.number + 1); vm.prank(USER); - creditFacade.closeCreditAccount(creditAccount, FRIEND, 10, false, calls); + creditFacade.closeCreditAccount(creditAccount, calls); assertEq0(targetMock.callData(), DUMB_CALLDATA, "Incorrect calldata"); } - /// @dev I:[CCA-4]: closeCreditAccount reverts on internal calls in multicall - function test_I_CCA_04_closeCreditAccount_reverts_on_internal_call_in_multicall_on_closure() public creditTest { - /// TODO: CHANGE TEST - // bytes memory DUMB_CALLDATA = abi.encodeWithSignature("hello(string)", "world"); - - // _openTestCreditAccount(); - - // vm.roll(block.number + 1); - - // vm.expectRevert(ForbiddenDuringClosureException.selector); - - // // It's used dumb calldata, cause all calls to creditFacade are forbidden - - // vm.prank(USER); - // creditFacade.closeCreditAccount( - // FRIEND, 0, true, MultiCallBuilder.build(MultiCall({target: address(creditFacade), callData: DUMB_CALLDATA})) - // ); - } - - /// @dev I:[CCA-5]: closeCreditAccount returns account to the end of AF1s remove borrower from creditAccounts mapping - function test_I_CCA_05_close_credit_account_updates_pool_correctly() public withAccountFactoryV1 creditTest { + /// @dev I:[CCA-5]: closeCreditAccount returns account to the factory and removes owner + function test_I_CCA_05_closeCreditAccount_returns_account_to_the_factory_and_removes_owner() + public + withAccountFactoryV1 + creditTest + { + address daiToken = tokenTestSuite.addressOf(Tokens.DAI); MultiCall[] memory calls = MultiCallBuilder.build( MultiCall({ target: address(creditFacade), @@ -205,9 +223,7 @@ contract CloseCreditAccountIntegrationTest is IntegrationTestHelper, ICreditFaca }), MultiCall({ target: address(creditFacade), - callData: abi.encodeCall( - ICreditFacadeV3Multicall.addCollateral, (tokenTestSuite.addressOf(Tokens.DAI), DAI_ACCOUNT_AMOUNT / 2) - ) + callData: abi.encodeCall(ICreditFacadeV3Multicall.addCollateral, (daiToken, DAI_ACCOUNT_AMOUNT / 2)) }) ); @@ -215,408 +231,31 @@ contract CloseCreditAccountIntegrationTest is IntegrationTestHelper, ICreditFaca vm.prank(USER); address creditAccount = creditFacade.openCreditAccount(USER, calls, 0); - assertTrue( - creditAccount - != AccountFactory(addressProvider.getAddressOrRevert(AP_ACCOUNT_FACTORY, NO_VERSION_CONTROL)).tail(), - "credit account is already in tail!" - ); + assertTrue(creditAccount != accountFactory.tail(), "credit account is already in tail!"); // Increase block number cause it's forbidden to close credit account in the same block vm.roll(block.number + 1); vm.prank(USER); - creditFacade.closeCreditAccount(creditAccount, USER, 0, false, new MultiCall[](0)); - - assertEq( + creditFacade.closeCreditAccount( creditAccount, - AccountFactory(addressProvider.getAddressOrRevert(AP_ACCOUNT_FACTORY, NO_VERSION_CONTROL)).tail(), - "credit account is not in accountFactory tail!" + MultiCallBuilder.build( + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall(ICreditFacadeV3Multicall.decreaseDebt, (type(uint256).max)) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall( + ICreditFacadeV3Multicall.withdrawCollateral, (daiToken, type(uint256).max, USER) + ) + }) + ) ); + assertEq(creditAccount, accountFactory.tail(), "credit account is not in accountFactory tail!"); + vm.expectRevert(CreditAccountDoesNotExistException.selector); creditManager.getBorrowerOrRevert(creditAccount); } - - /// @dev I:[CCA-6]: closeCreditAccount returns undelying tokens if credit account balance > amounToPool - /// - /// This test covers the case: - /// Closure type: CLOSURE - /// Underlying balance: > amountToPool - /// Send all assets: false - /// - function test_I_CCA_06_close_credit_account_returns_underlying_token_if_not_liquidated() public creditTest { - MultiCall[] memory calls = MultiCallBuilder.build( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeV3Multicall.increaseDebt, (DAI_ACCOUNT_AMOUNT)) - }), - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall( - ICreditFacadeV3Multicall.addCollateral, (tokenTestSuite.addressOf(Tokens.DAI), DAI_ACCOUNT_AMOUNT / 2) - ) - }) - ); - - // Existing address case - vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(USER, calls, 0); - - uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(pool)); - uint256 friendBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, FRIEND); - - (uint256 debt, uint256 cumulativeIndexLastUpdate,,,,,,) = creditManager.creditAccountInfo(creditAccount); - - vm.warp(block.timestamp + 365 days); - vm.roll(block.number + 1); - - uint256 cumulativeIndexAtClose = pool.baseInterestIndex(); - - uint256 interestAccrued = (debt * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - debt; - - (uint16 feeInterest,,,,) = creditManager.fees(); - - uint256 profit = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - - uint256 amountToPool = debt + interestAccrued + profit; - - vm.expectCall(address(pool), abi.encodeCall(IPoolV3.repayCreditAccount, (debt, profit, 0))); - - vm.prank(USER); - creditFacade.closeCreditAccount(creditAccount, FRIEND, 0, false, new MultiCall[](0)); - - expectBalance(Tokens.DAI, creditAccount, 1); - expectBalance(Tokens.DAI, address(pool), poolBalanceBefore + amountToPool); - expectBalance( - Tokens.DAI, - FRIEND, - friendBalanceBefore + 3 * DAI_ACCOUNT_AMOUNT / 2 - amountToPool - 1, - "Incorrect amount were paid back" - ); - } - - /// @dev I:[CCA-7]: closeCreditAccount sets correct values and transfers tokens from pool - /// - /// This test covers the case: - /// Closure type: CLOSURE - /// Underlying balance: < amountToPool - /// Send all assets: false - /// - function test_I_CCA_07_close_credit_account_charges_caller_if_underlying_token_not_enough() public creditTest { - MultiCall[] memory calls = MultiCallBuilder.build( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeV3Multicall.increaseDebt, (DAI_ACCOUNT_AMOUNT)) - }), - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall( - ICreditFacadeV3Multicall.addCollateral, (tokenTestSuite.addressOf(Tokens.DAI), DAI_ACCOUNT_AMOUNT / 2) - ) - }) - ); - - // Existing address case - vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(USER, calls, 0); - - tokenTestSuite.mint(Tokens.DAI, USER, 2 * DAI_ACCOUNT_AMOUNT); - - uint256 userBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, USER); - uint256 friendBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, FRIEND); - - (uint256 debt, uint256 cumulativeIndexLastUpdate,,,,,,) = creditManager.creditAccountInfo(creditAccount); - - uint256 rate = pool.baseInterestRate(); - - /// amount on account is 3*2*DAI_ACCOUNT_AMOUNT - /// debt is DAI_ACCOUNT_AMOUNT - uint256 warpTime = RAY / rate * SECONDS_PER_YEAR; - - vm.warp(block.timestamp + warpTime); - vm.roll(block.number + 1); - - uint256 cumulativeIndexAtClose = pool.baseInterestIndex(); - - uint256 interestAccrued = (debt * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - debt; - - uint256 amountToPool; - { - (uint16 feeInterest,,,,) = creditManager.fees(); - - uint256 profit = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - - amountToPool = debt + interestAccrued + profit; - - console.log(amountToPool * 10_000 / DAI_ACCOUNT_AMOUNT); - - uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(pool)); - - vm.expectCall(address(pool), abi.encodeCall(IPoolV3.repayCreditAccount, (debt, profit, 0))); - - vm.prank(USER); - creditFacade.closeCreditAccount(creditAccount, FRIEND, 0, false, new MultiCall[](0)); - - expectBalance(Tokens.DAI, creditAccount, 1, "Credit account balance != 1"); - - expectBalance(Tokens.DAI, address(pool), poolBalanceBefore + amountToPool); - } - - expectBalance( - Tokens.DAI, - USER, - userBalanceBefore + 3 * DAI_ACCOUNT_AMOUNT / 2 - amountToPool - 1, - "Incorrect amount was charged from user" - ); - - expectBalance(Tokens.DAI, FRIEND, friendBalanceBefore, "Incorrect amount were paid back"); - } - - /// @dev I:[CCA-8]: liquidateCreditAccount sets correct values and transfers tokens from pool - /// - /// This test covers the case: - /// Closure type: LIQUIDATION - /// Underlying balance: < amountToPool - /// Send all assets: false - /// Remaining funds: 0 - /// - function test_I_CCA_08_liquidate_credit_account_charges_caller_if_underlying_token_not_enough() public creditTest { - uint256 friendBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, FRIEND); - - uint256 interestAccrued; - - MultiCall[] memory calls = MultiCallBuilder.build( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeV3Multicall.increaseDebt, (DAI_ACCOUNT_AMOUNT)) - }), - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall( - ICreditFacadeV3Multicall.addCollateral, (tokenTestSuite.addressOf(Tokens.DAI), DAI_ACCOUNT_AMOUNT / 2) - ) - }) - ); - - // Existing address case - vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(USER, calls, 0); - - tokenTestSuite.mint(Tokens.DAI, LIQUIDATOR, 2 * DAI_ACCOUNT_AMOUNT); - uint256 debt; - { - uint256 cumulativeIndexLastUpdate; - (debt, cumulativeIndexLastUpdate,,,,,,) = creditManager.creditAccountInfo(creditAccount); - - uint256 rate = pool.baseInterestRate(); - - if (!expirable) { - /// amount on account is 3*2*DAI_ACCOUNT_AMOUNT - /// debt is DAI_ACCOUNT_AMOUNT - uint256 warpTime = RAY / rate * SECONDS_PER_YEAR; - - vm.warp(block.timestamp + warpTime); - - address pf = priceOracle.priceFeeds(tokenTestSuite.addressOf(Tokens.DAI)); - PriceFeedMock(pf).setParams(0, 0, block.timestamp, 0); - } - - vm.roll(block.number + 1); - - uint256 cumulativeIndexAtClose = pool.baseInterestIndex(); - - interestAccrued = (debt * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - debt; - } - - uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(pool)); - uint256 userBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, USER); - uint256 discount; - - { - (,, uint16 liquidationDiscount,, uint16 liquidationDiscountExpired) = creditManager.fees(); - discount = expirable ? liquidationDiscountExpired : liquidationDiscount; - } - - uint256 totalValue = (3 * DAI_ACCOUNT_AMOUNT / 2 - 1) / (10 ** 10) * (10 ** 10); - uint256 amountToPool = (totalValue * discount) / PERCENTAGE_FACTOR; - - { - uint256 loss = debt + interestAccrued - amountToPool; - - vm.expectCall(address(pool), abi.encodeCall(IPoolV3.repayCreditAccount, (debt, 0, loss))); - } - - vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 0, false, new MultiCall[](0)); - - assertEq(tokenTestSuite.balanceOf(Tokens.DAI, USER), userBalanceBefore, "Remaining funds is not zero!"); - - expectBalance(Tokens.DAI, creditAccount, 1, "Credit account balance != 1"); - - expectBalance(Tokens.DAI, address(pool), poolBalanceBefore + amountToPool); - - // uint256 expectedFriendBalance = friendBalanceBefore - // + (totalValue * (PERCENTAGE_FACTOR - discount)) / PERCENTAGE_FACTOR - (i == 2 ? 0 : 1); - - uint256 expectedFriendBalance = friendBalanceBefore + 3 * DAI_ACCOUNT_AMOUNT / 2 - amountToPool - 1; - - expectBalance( - Tokens.DAI, FRIEND, expectedFriendBalance, "Incorrect amount were paid to liqiudator friend address" - ); - } - - /// @dev I:[CCA-9]: closeCreditAccount sends assets depends on sendAllAssets flag - /// - /// This test covers the case: - /// Closure type: LIQUIDATION - /// Underlying balance: < amountToPool - /// Send all assets: false - /// Remaining funds: >0 - /// - - function test_I_CCA_09_close_credit_account_with_nonzero_skipTokenMask_sends_correct_tokens() public creditTest { - // (uint256 debt,, address creditAccount) = _openCreditAccount(); - - // tokenTestSuite.mint(Tokens.DAI, creditAccount, debt); - // tokenTestSuite.mint(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); - - // tokenTestSuite.mint(Tokens.USDC, creditAccount, USDC_EXCHANGE_AMOUNT); - - // tokenTestSuite.mint(Tokens.LINK, creditAccount, LINK_EXCHANGE_AMOUNT); - - // uint256 wethTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - // uint256 usdcTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)); - // uint256 linkTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - - // CollateralDebtData memory collateralDebtData; - // collateralDebtData.debt = debt; - // collateralDebtData.accruedInterest = 0; - // collateralDebtData.accruedFees = 0; - // collateralDebtData.enabledTokensMask = wethTokenMask | usdcTokenMask | linkTokenMask; - - // creditManager.closeCreditAccount({ - // creditAccount: creditAccount, - // closureAction: ClosureAction.CLOSE_ACCOUNT, - // collateralDebtData: collateralDebtData, - // payer: USER, - // to: FRIEND, - // skipTokensMask: wethTokenMask | usdcTokenMask, - // convertToETH: false - // }); - - // expectBalance(Tokens.WETH, FRIEND, 0); - // expectBalance(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); - - // expectBalance(Tokens.USDC, FRIEND, 0); - // expectBalance(Tokens.USDC, creditAccount, USDC_EXCHANGE_AMOUNT); - - // expectBalance(Tokens.LINK, FRIEND, LINK_EXCHANGE_AMOUNT - 1); - } - - /// @dev I:[CCA-10]: closeCreditAccount sends ETH for WETH creditManger to borrower - /// CASE: CLOSURE - /// Underlying token: WETH - function test_I_CCA_10_close_weth_credit_account_sends_eth_to_borrower() public creditTest { - // // It takes "clean" address which doesn't holds any assets - - // // _connectCreditManagerSuite(Tokens.WETH); - - // /// CLOSURE CASE - // (uint256 debt, uint256 cumulativeIndexLastUpdate, address creditAccount) = _openCreditAccount(); - - // // Transfer additional debt. After that underluying token balance = 2 * debt - // tokenTestSuite.mint(Tokens.WETH, creditAccount, debt); - - // vm.warp(block.timestamp + 365 days); - - // uint256 cumulativeIndexAtClose = pool.baseInterestIndex(); - - // uint256 interestAccrued = (debt * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - debt; - - // // creditManager.closeCreditAccount(USER, ClosureAction.CLOSE_ACCOUNT, 0, USER, USER, 0, true); - - // // creditManager.closeCreditAccount( - // // creditAccount, ClosureAction.CLOSE_ACCOUNT, 0, USER, USER, 1, 0, debt + interestAccrued, true - // // ); - - // (uint16 feeInterest,,,,) = creditManager.fees(); - // uint256 profit = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - - // CollateralDebtData memory collateralDebtData; - // collateralDebtData.debt = debt; - // collateralDebtData.accruedInterest = interestAccrued; - // collateralDebtData.accruedFees = profit; - // collateralDebtData.enabledTokensMask = UNDERLYING_TOKEN_MASK; - - // creditManager.closeCreditAccount({ - // creditAccount: creditAccount, - // closureAction: ClosureAction.CLOSE_ACCOUNT, - // collateralDebtData: collateralDebtData, - // payer: USER, - // to: USER, - // skipTokensMask: 0, - // convertToETH: true - // }); - - // expectBalance(Tokens.WETH, creditAccount, 1); - - // uint256 amountToPool = debt + interestAccrued + profit; - - // assertEq( - // withdrawalManager.immediateWithdrawals(address(creditFacade), tokenTestSuite.addressOf(Tokens.WETH)), - // 2 * debt - amountToPool - 1, - // "Incorrect amount deposited to withdrawalManager" - // ); - } - - /// @dev I:[CCA-11]: closeCreditAccount sends ETH for WETH creditManger to borrower - /// CASE: CLOSURE - /// Underlying token: DAI - function test_I_CCA_11_close_dai_credit_account_sends_eth_to_borrower() public creditTest { - // /// CLOSURE CASE - // (uint256 debt,, address creditAccount) = _openCreditAccount(); - - // // Transfer additional debt. After that underluying token balance = 2 * debt - // tokenTestSuite.mint(Tokens.DAI, creditAccount, debt); - - // // Adds WETH to test how it would be converted - // tokenTestSuite.mint(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); - - // uint256 wethTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - // uint256 daiTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)); - - // CollateralDebtData memory collateralDebtData; - // collateralDebtData.debt = debt; - // collateralDebtData.accruedInterest = 0; - // collateralDebtData.accruedFees = 0; - // collateralDebtData.enabledTokensMask = wethTokenMask | daiTokenMask; - - // creditManager.closeCreditAccount({ - // creditAccount: creditAccount, - // closureAction: ClosureAction.CLOSE_ACCOUNT, - // collateralDebtData: collateralDebtData, - // payer: USER, - // to: USER, - // skipTokensMask: 0, - // convertToETH: true - // }); - - // expectBalance(Tokens.WETH, creditAccount, 1); - - // assertEq( - // withdrawalManager.immediateWithdrawals(address(creditFacade), tokenTestSuite.addressOf(Tokens.WETH)), - // WETH_EXCHANGE_AMOUNT - 1, - // "Incorrect amount deposited to withdrawalManager" - // ); - } - - // function test_I_CM_65_closeCreditAccount_reverts_when_paused_and_liquidator_tries_to_close() public creditTest { - // vm.startPrank(CONFIGURATOR); - // creditManager.pause(); - // creditManager.addEmergencyLiquidator(LIQUIDATOR); - // vm.stopPrank(); - - // vm.expectRevert("Pausable: paused"); - // // creditManager.closeCreditAccount(USER, ClosureAction.CLOSE_ACCOUNT, 0, LIQUIDATOR, FRIEND, 0, false); - // } } diff --git a/contracts/test/integration/credit/LiquidateCreditAccount.int.t.sol b/contracts/test/integration/credit/LiquidateCreditAccount.int.t.sol index 1b290d97..0bb79d76 100644 --- a/contracts/test/integration/credit/LiquidateCreditAccount.int.t.sol +++ b/contracts/test/integration/credit/LiquidateCreditAccount.int.t.sol @@ -8,7 +8,6 @@ import {ICreditAccountBase} from "../../../interfaces/ICreditAccountV3.sol"; import { ICreditManagerV3, ICreditManagerV3Events, - ClosureAction, ManageDebtAction, BOT_PERMISSIONS_SET_FLAG } from "../../../interfaces/ICreditManagerV3.sol"; @@ -31,7 +30,7 @@ contract LiquidateCreditAccountIntegrationTest is IntegrationTestHelper, ICredit function test_I_LCA_01_liquidateCreditAccount_reverts_if_credit_account_not_exists() public creditTest { vm.expectRevert(CreditAccountDoesNotExistException.selector); vm.prank(USER); - creditFacade.liquidateCreditAccount(DUMB_ADDRESS, DUMB_ADDRESS, 0, false, MultiCallBuilder.build()); + creditFacade.liquidateCreditAccount(DUMB_ADDRESS, DUMB_ADDRESS, MultiCallBuilder.build()); } /// @dev I:[LCA-2]: liquidateCreditAccount reverts if hf > 1 @@ -41,7 +40,7 @@ contract LiquidateCreditAccountIntegrationTest is IntegrationTestHelper, ICredit vm.expectRevert(CreditAccountNotLiquidatableException.selector); vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount(creditAccount, LIQUIDATOR, 0, true, MultiCallBuilder.build()); + creditFacade.liquidateCreditAccount(creditAccount, LIQUIDATOR, MultiCallBuilder.build()); } /// @dev I:[LCA-3]: liquidateCreditAccount executes needed calls and emits events @@ -58,9 +57,7 @@ contract LiquidateCreditAccountIntegrationTest is IntegrationTestHelper, ICredit creditFacade.setBotPermissions({ creditAccount: creditAccount, bot: address(adapterMock), - permissions: uint192(ADD_COLLATERAL_PERMISSION), - totalFundingAllowance: 0, - weeklyFundingAllowance: 0 + permissions: uint192(ADD_COLLATERAL_PERMISSION) }); MultiCall[] memory calls = MultiCallBuilder.build( @@ -88,10 +85,6 @@ contract LiquidateCreditAccountIntegrationTest is IntegrationTestHelper, ICredit vm.expectEmit(false, false, false, false); emit FinishMultiCall(); - vm.expectCall( - address(botList), abi.encodeCall(BotListV3.eraseAllBotPermissions, (address(creditManager), creditAccount)) - ); - vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (address(1)))); // Total value = 2 * DAI_ACCOUNT_AMOUNT, cause we have x2 leverage @@ -117,10 +110,10 @@ contract LiquidateCreditAccountIntegrationTest is IntegrationTestHelper, ICredit // ); vm.expectEmit(true, true, true, true); - emit LiquidateCreditAccount(creditAccount, USER, LIQUIDATOR, FRIEND, ClosureAction.LIQUIDATE_ACCOUNT, 0); + emit LiquidateCreditAccount(creditAccount, USER, LIQUIDATOR, FRIEND, 0); vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 10, false, calls); + creditFacade.liquidateCreditAccount(creditAccount, FRIEND, calls); } /// @dev I:[LCA-4]: Borrowing is prohibited after a liquidation with loss @@ -138,7 +131,7 @@ contract LiquidateCreditAccountIntegrationTest is IntegrationTestHelper, ICredit _makeAccountsLiquitable(); vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 10, false, calls); + creditFacade.liquidateCreditAccount(creditAccount, FRIEND, calls); maxDebtPerBlockMultiplier = creditFacade.maxDebtPerBlockMultiplier(); @@ -163,7 +156,7 @@ contract LiquidateCreditAccountIntegrationTest is IntegrationTestHelper, ICredit _makeAccountsLiquitable(); vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 10, false, calls); + creditFacade.liquidateCreditAccount(creditAccount, FRIEND, calls); assertTrue(creditFacade.paused(), "Credit manager was not paused"); } @@ -178,410 +171,18 @@ contract LiquidateCreditAccountIntegrationTest is IntegrationTestHelper, ICredit MultiCall[] memory calls = MultiCallBuilder.build( MultiCall({ target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeV3Multicall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) + callData: abi.encodeCall(ICreditFacadeV3Multicall.increaseDebt, (DAI_ACCOUNT_AMOUNT)) }) ); (address creditAccount,) = _openTestCreditAccount(); _makeAccountsLiquitable(); - vm.expectRevert(abi.encodeWithSelector(NoPermissionException.selector, ADD_COLLATERAL_PERMISSION)); + vm.expectRevert(abi.encodeWithSelector(NoPermissionException.selector, INCREASE_DEBT_PERMISSION)); vm.prank(LIQUIDATOR); // It's used dumb calldata, cause all calls to creditFacade are forbidden - creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 10, true, calls); - } - - /// @dev I:[LCA-7]: liquidateCreditAccount sets correct values and transfers tokens from pool - /// - /// This test covers the case: - /// Closure type: LIQUIDATION / LIQUIDATION_EXPIRED - /// Underlying balance: < amountToPool - /// Send all assets: false - /// Remaining funds: >0 - /// - - function test_I_LCA_07_liquidate_credit_account_charges_caller_if_underlying_token_not_enough() public creditTest { - // uint256 debt; - // address creditAccount; - - // uint256 expectedRemainingFunds = 100 * WAD; - - // uint256 profit; - // uint256 amountToPool; - // uint256 totalValue; - // uint256 interestAccrued; - - // { - // uint256 cumulativeIndexLastUpdate; - // (debt, cumulativeIndexLastUpdate, creditAccount) = _openCreditAccount(); - - // vm.warp(block.timestamp + 365 days); - - // uint256 cumulativeIndexAtClose = pool.baseInterestIndex(); - - // interestAccrued = (debt * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - debt; - - // uint16 feeInterest; - // uint16 feeLiquidation; - // uint16 liquidationDiscount; - - // { - // (feeInterest,,,,) = creditManager.fees(); - // } - - // { - // uint16 feeLiquidationNormal; - // uint16 feeLiquidationExpired; - - // (, feeLiquidationNormal,, feeLiquidationExpired,) = creditManager.fees(); - - // feeLiquidation = expirable ? feeLiquidationExpired : feeLiquidationNormal; - // } - - // { - // uint16 liquidationDiscountNormal; - // uint16 liquidationDiscountExpired; - - // (feeInterest,, liquidationDiscountNormal,, liquidationDiscountExpired) = creditManager.fees(); - - // liquidationDiscount = expirable ? liquidationDiscountExpired : liquidationDiscountNormal; - // } - - // uint256 profitInterest = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - - // amountToPool = debt + interestAccrued + profitInterest; - - // totalValue = - // ((amountToPool + expectedRemainingFunds) * PERCENTAGE_FACTOR) / (liquidationDiscount - feeLiquidation); - - // uint256 profitLiquidation = (totalValue * feeLiquidation) / PERCENTAGE_FACTOR; - - // amountToPool += profitLiquidation; - - // profit = profitInterest + profitLiquidation; - // } - - // uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(pool)); - - // tokenTestSuite.mint(Tokens.DAI, LIQUIDATOR, totalValue); - // expectBalance(Tokens.DAI, USER, 0, "USER has non-zero balance"); - // expectBalance(Tokens.DAI, FRIEND, 0, "FRIEND has non-zero balance"); - // expectBalance(Tokens.DAI, LIQUIDATOR, totalValue, "LIQUIDATOR has incorrect initial balance"); - - // expectBalance(Tokens.DAI, creditAccount, debt, "creditAccount has incorrect initial balance"); - - // uint256 remainingFunds; - - // { - // uint256 loss; - - // (uint16 feeInterest,,,,) = creditManager.fees(); - - // CollateralDebtData memory collateralDebtData; - // collateralDebtData.debt = debt; - // collateralDebtData.accruedInterest = interestAccrued; - // collateralDebtData.accruedFees = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - // collateralDebtData.totalValue = totalValue; - // collateralDebtData.enabledTokensMask = UNDERLYING_TOKEN_MASK; - - // vm.expectCall(address(pool), abi.encodeCall(IPoolService.repayCreditAccount, (debt, profit, 0))); - - // // (remainingFunds, loss) = creditManager.closeCreditAccount({ - // // creditAccount: creditAccount, - // // closureAction: i == 1 ? ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT : ClosureAction.LIQUIDATE_ACCOUNT, - // // collateralDebtData: collateralDebtData, - // // payer: LIQUIDATOR, - // // to: FRIEND, - // // skipTokensMask: 0, - // // convertToETH: false - // // }); - - // assertLe(expectedRemainingFunds - remainingFunds, 2, "Incorrect remaining funds"); - - // assertEq(loss, 0, "Loss can't be positive with remaining funds"); - // } - - // { - // expectBalance(Tokens.DAI, creditAccount, 1, "Credit account balance != 1"); - // expectBalance(Tokens.DAI, USER, remainingFunds, "USER get incorrect amount as remaning funds"); - - // expectBalance(Tokens.DAI, address(pool), poolBalanceBefore + amountToPool, "INCORRECT POOL BALANCE"); - // } - - // expectBalance( - // Tokens.DAI, - // LIQUIDATOR, - // totalValue + debt - amountToPool - remainingFunds - 1, - // "Incorrect amount were paid to lqiudaidator" - // ); - } - - /// @dev I:[LCA-8]: liquidateCreditAccount sends ETH for WETH creditManger to borrower - /// CASE: LIQUIDATION - function test_I_LCA_08_liquidate_credit_account_sends_eth_to_liquidator_and_weth_to_borrower() public creditTest { - // /// Store USER ETH balance - - // // uint256 userBalanceBefore = tokenTestSuite.balanceOf(Tokens.WETH, USER); - - // (,, uint16 liquidationDiscount,,) = creditManager.fees(); - - // // It takes "clean" address which doesn't holds any assets - - // // _connectCreditManagerSuite(Tokens.WETH); - - // /// CLOSURE CASE - // (uint256 debt,, address creditAccount) = _openCreditAccount(); - - // // Transfer additional debt. After that underluying token balance = 2 * debt - // tokenTestSuite.mint(Tokens.WETH, creditAccount, debt); - - // uint256 totalValue = debt * 2; - - // uint256 wethTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - // uint256 daiTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)); - - // CollateralDebtData memory collateralDebtData; - // collateralDebtData.debt = debt; - // collateralDebtData.accruedInterest = 0; - // collateralDebtData.accruedFees = 0; - // collateralDebtData.totalValue = totalValue; - // collateralDebtData.enabledTokensMask = wethTokenMask | daiTokenMask; - - // creditManager.closeCreditAccount({ - // creditAccount: creditAccount, - // closureAction: ClosureAction.LIQUIDATE_ACCOUNT, - // collateralDebtData: collateralDebtData, - // payer: LIQUIDATOR, - // to: FRIEND, - // skipTokensMask: 0, - // convertToETH: true - // }); - - // // checks that no eth were sent to USER account - // expectEthBalance(USER, 0); - - // expectBalance(Tokens.WETH, creditAccount, 1, "Credit account balance != 1"); - - // // expectBalance(Tokens.WETH, USER, userBalanceBefore + remainingFunds, "Incorrect amount were paid back"); - - // assertEq( - // withdrawalManager.immediateWithdrawals(address(creditFacade), tokenTestSuite.addressOf(Tokens.WETH)), - // (totalValue * (PERCENTAGE_FACTOR - liquidationDiscount)) / PERCENTAGE_FACTOR, - // "Incorrect amount were paid to liqiudator friend address" - // ); + creditFacade.liquidateCreditAccount(creditAccount, FRIEND, calls); } - - /// @dev I:[LCA-9]: liquidateCreditAccount sends ETH for WETH creditManger to borrower - /// CASE: LIQUIDATION - /// Underlying token: DAI - function test_I_LCA_09_liquidate_dai_credit_account_sends_eth_to_liquidator() public creditTest { - // /// CLOSURE CASE - // (uint256 debt,, address creditAccount) = _openCreditAccount(); - // // creditManager.transferAccountOwnership(creditAccount, address(this)); - - // // Transfer additional debt. After that underluying token balance = 2 * debt - // tokenTestSuite.mint(Tokens.DAI, creditAccount, debt); - - // // Adds WETH to test how it would be converted - // tokenTestSuite.mint(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); - - // // creditManager.transferAccountOwnership(creditAccount, USER); - // uint256 wethTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - // uint256 daiTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)); - - // CollateralDebtData memory collateralDebtData; - // collateralDebtData.debt = debt; - // collateralDebtData.accruedInterest = 0; - // collateralDebtData.accruedFees = 0; - // collateralDebtData.totalValue = debt; - // collateralDebtData.enabledTokensMask = wethTokenMask | daiTokenMask; - - // creditManager.closeCreditAccount({ - // creditAccount: creditAccount, - // closureAction: ClosureAction.LIQUIDATE_ACCOUNT, - // collateralDebtData: collateralDebtData, - // payer: LIQUIDATOR, - // to: FRIEND, - // skipTokensMask: 0, - // convertToETH: true - // }); - - // expectBalance(Tokens.WETH, creditAccount, 1); - - // assertEq( - // withdrawalManager.immediateWithdrawals(address(creditFacade), tokenTestSuite.addressOf(Tokens.WETH)), - // WETH_EXCHANGE_AMOUNT - 1, - // "Incorrect amount were paid to liqiudator friend address" - // ); - } - - /// @dev I:[FA-47]: liquidateExpiredCreditAccount should not work before the CreditFacadeV3 is expired - function test_I_FA_47_liquidateExpiredCreditAccount_reverts_before_expiration() public expirableCase creditTest { - // _setUp({ - // _underlying: Tokens.DAI, - // withDegenNFT: false, - // withExpiration: true, - // supportQuotas: false, - // accountFactoryVer: 1 - // }); - - _openTestCreditAccount(); - - // vm.expectRevert(CantLiquidateNonExpiredException.selector); - - // vm.prank(LIQUIDATOR); - // creditFacade.liquidateExpiredCreditAccount(USER, LIQUIDATOR, 0, false, MultiCallBuilder.build()); - } - - /// @dev I:[FA-48]: liquidateExpiredCreditAccount should not work when expiration is set to zero (i.e. CreditFacadeV3 is non-expiring) - function test_I_FA_48_liquidateExpiredCreditAccount_reverts_on_CreditFacade_with_no_expiration() - public - creditTest - { - _openTestCreditAccount(); - - // vm.expectRevert(CantLiquidateNonExpiredException.selector); - - // vm.prank(LIQUIDATOR); - // creditFacade.liquidateExpiredCreditAccount(USER, LIQUIDATOR, 0, false, MultiCallBuilder.build()); - } - - /// @dev I:[FA-49]: liquidateExpiredCreditAccount works correctly and emits events - function test_I_FA_49_liquidateExpiredCreditAccount_works_correctly_after_expiration() public creditTest { - // _setUp({ - // _underlying: Tokens.DAI, - // withDegenNFT: false, - // withExpiration: true, - // supportQuotas: false, - // accountFactoryVer: 1 - // }); - // (address creditAccount, uint256 balance) = _openTestCreditAccount(); - - // bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); - - // MultiCall[] memory calls = MultiCallBuilder.build( - // MultiCall({target: address(adapterMock), callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))}) - // ); - - vm.warp(block.timestamp + 1); - vm.roll(block.number + 1); - - // (uint256 borrowedAmount, uint256 borrowedAmountWithInterest,) = - // creditManager.calcAccruedInterestAndFees(creditAccount); - - // (, uint256 remainingFunds,,) = creditManager.calcClosePayments( - // balance, ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT, borrowedAmount, borrowedAmountWithInterest - // ); - - // // EXPECTED STACK TRACE & EVENTS - - // vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (creditAccount))); - - // vm.expectEmit(true, false, false, false); - // emit StartMultiCall(creditAccount); - - // vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.execute, (DUMB_CALLDATA))); - - // vm.expectEmit(true, false, false, false); - // emit Execute(address(targetMock)); - - // vm.expectCall(creditAccount, abi.encodeCall(ICreditAccountBase.execute, (address(targetMock), DUMB_CALLDATA))); - - // vm.expectCall(address(targetMock), DUMB_CALLDATA); - - // vm.expectEmit(false, false, false, false); - // emit FinishMultiCall(); - - // vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (address(1)))); - // // Total value = 2 * DAI_ACCOUNT_AMOUNT, cause we have x2 leverage - // uint256 totalValue = balance; - - // // vm.expectCall( - // // address(creditManager), - // // abi.encodeCall( - // // ICreditManagerV3.closeCreditAccount, - // // ( - // // creditAccount, - // // ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT, - // // totalValue, - // // LIQUIDATOR, - // // FRIEND, - // // 1, - // // 10, - // // DAI_ACCOUNT_AMOUNT, - // // true - // // ) - // // ) - // // ); - - // vm.expectEmit(true, true, false, true); - // emit LiquidateCreditAccount( - // creditAccount, USER, LIQUIDATOR, FRIEND, ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT, remainingFunds - // ); - - // vm.prank(LIQUIDATOR); - // creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 10, true, calls); - } - - // /// @dev I:[FA-56]: liquidateCreditAccount correctly uses BlacklistHelper during liquidations - // function test_I_FA_56_liquidateCreditAccount_correctly_handles_blacklisted_borrowers() public creditTest { - // _setUp(Tokens.USDC); - - // cft.testFacadeWithBlacklistHelper(); - - // creditFacade = cft.creditFacade(); - - // address usdc = tokenTestSuite.addressOf(Tokens.USDC); - - // address blacklistHelper = creditFacade.blacklistHelper(); - - // _openTestCreditAccount(); - - // uint256 expectedAmount = ( - // 2 * USDC_ACCOUNT_AMOUNT * (PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM - DEFAULT_FEE_LIQUIDATION) - // ) / PERCENTAGE_FACTOR - USDC_ACCOUNT_AMOUNT - 1 - 1; // second -1 because we add 1 to helper balance - - // vm.roll(block.number + 1); - - // vm.prank(address(creditConfigurator)); - // CreditManagerV3(address(creditManager)).setLiquidationThreshold(usdc, 1); - - // ERC20BlacklistableMock(usdc).setBlacklisted(USER, true); - - // vm.expectCall(blacklistHelper, abi.encodeCall(IWithdrawalManagerV3.isBlacklisted, (usdc, USER))); - - // vm.expectCall( - // address(creditManager), abi.encodeCall(ICreditManagerV3.transferAccountOwnership, (USER, blacklistHelper)) - // ); - - // vm.expectCall(blacklistHelper, abi.encodeCall(IWithdrawalManagerV3.addWithdrawal, (usdc, USER, expectedAmount))); - - // vm.expectEmit(true, false, false, true); - // emit UnderlyingSentToBlacklistHelper(USER, expectedAmount); - - // vm.prank(LIQUIDATOR); - // creditFacade.liquidateCreditAccount(USER, FRIEND, 0, true, MultiCallBuilder.build()); - - // assertEq(IWithdrawalManagerV3(blacklistHelper).claimable(usdc, USER), expectedAmount, "Incorrect claimable amount"); - - // vm.prank(USER); - // IWithdrawalManagerV3(blacklistHelper).claim(usdc, FRIEND2); - - // assertEq(tokenTestSuite.balanceOf(Tokens.USDC, FRIEND2), expectedAmount, "Transferred amount incorrect"); - // } - - // /// @dev I:[CM-64]: closeCreditAccount reverts when attempting to liquidate while paused, - // /// and the payer is not set as emergency liquidator - - // function test_I_CM_64_closeCreditAccount_reverts_when_paused_and_liquidator_not_privileged() public { - // vm.prank(CONFIGURATOR); - // creditManager.pause(); - - // vm.expectRevert("Pausable: paused"); - // // creditManager.closeCreditAccount(USER, ClosureAction.LIQUIDATE_ACCOUNT, 0, LIQUIDATOR, FRIEND, 0, false); - // } - - // /// @dev I:[CM-65]: Emergency liquidator can't close an account instead of liquidating } diff --git a/contracts/test/integration/credit/ManageDebt.int.t.sol b/contracts/test/integration/credit/ManageDebt.int.t.sol index 5c26abab..cee823c3 100644 --- a/contracts/test/integration/credit/ManageDebt.int.t.sol +++ b/contracts/test/integration/credit/ManageDebt.int.t.sol @@ -6,7 +6,6 @@ pragma solidity ^0.8.17; import { ICreditManagerV3, ICreditManagerV3Events, - ClosureAction, ManageDebtAction, BOT_PERMISSIONS_SET_FLAG } from "../../../interfaces/ICreditManagerV3.sol"; @@ -36,7 +35,7 @@ contract ManegDebtIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Event vm.expectCall( address(creditManager), abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, 1, new uint256[](0), PERCENTAGE_FACTOR) + ICreditManagerV3.fullCollateralCheck, (creditAccount, 1, new uint256[](0), PERCENTAGE_FACTOR, false) ) ); @@ -174,7 +173,7 @@ contract ManegDebtIntegrationTest is IntegrationTestHelper, ICreditFacadeV3Event vm.expectCall( address(creditManager), abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, 1, new uint256[](0), PERCENTAGE_FACTOR) + ICreditManagerV3.fullCollateralCheck, (creditAccount, 1, new uint256[](0), PERCENTAGE_FACTOR, false) ) ); diff --git a/contracts/test/integration/credit/Multicall.int.t.sol b/contracts/test/integration/credit/Multicall.int.t.sol index 097b6bf1..d9203ae2 100644 --- a/contracts/test/integration/credit/Multicall.int.t.sol +++ b/contracts/test/integration/credit/Multicall.int.t.sol @@ -8,7 +8,6 @@ import {ICreditAccountBase} from "../../../interfaces/ICreditAccountV3.sol"; import { ICreditManagerV3, ICreditManagerV3Events, - ClosureAction, ManageDebtAction, BOT_PERMISSIONS_SET_FLAG } from "../../../interfaces/ICreditManagerV3.sol"; @@ -181,7 +180,7 @@ contract MultiCallIntegrationTest is vm.expectCall( address(creditManager), abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, 3, new uint256[](0), PERCENTAGE_FACTOR) + ICreditManagerV3.fullCollateralCheck, (creditAccount, 3, new uint256[](0), PERCENTAGE_FACTOR, false) ) ); @@ -242,7 +241,7 @@ contract MultiCallIntegrationTest is vm.expectCall( address(creditManager), abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, 3, new uint256[](0), PERCENTAGE_FACTOR) + ICreditManagerV3.fullCollateralCheck, (creditAccount, 3, new uint256[](0), PERCENTAGE_FACTOR, false) ) ); @@ -318,7 +317,7 @@ contract MultiCallIntegrationTest is vm.expectCall( address(creditManager), abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, 1, new uint256[](0), PERCENTAGE_FACTOR) + ICreditManagerV3.fullCollateralCheck, (creditAccount, 1, new uint256[](0), PERCENTAGE_FACTOR, false) ) ); @@ -524,7 +523,7 @@ contract MultiCallIntegrationTest is vm.expectCall( address(creditManager), abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, enabledTokensMap, collateralHints, 10001) + ICreditManagerV3.fullCollateralCheck, (creditAccount, enabledTokensMap, collateralHints, 10001, false) ) ); @@ -552,10 +551,11 @@ contract MultiCallIntegrationTest is vm.expectCall( address(creditManager), abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, enabledTokensMap, collateralHints, 10001) + ICreditManagerV3.fullCollateralCheck, (creditAccount, enabledTokensMap, collateralHints, 10001, false) ) ); + vm.expectRevert(InvalidCollateralHintException.selector); vm.prank(USER); creditFacade.multicall( creditAccount, diff --git a/contracts/test/integration/credit/OpenCreditAccount.int.t.sol b/contracts/test/integration/credit/OpenCreditAccount.int.t.sol index f8e14237..8db696eb 100644 --- a/contracts/test/integration/credit/OpenCreditAccount.int.t.sol +++ b/contracts/test/integration/credit/OpenCreditAccount.int.t.sol @@ -12,7 +12,6 @@ import {ICreditAccount} from "@gearbox-protocol/core-v2/contracts/interfaces/ICr import { ICreditManagerV3, ICreditManagerV3Events, - ClosureAction, CollateralTokenData, ManageDebtAction, CollateralDebtData @@ -248,7 +247,7 @@ contract OpenCreditAccountIntegrationTest is IntegrationTestHelper, ICreditFacad address(creditManager), abi.encodeCall( ICreditManagerV3.fullCollateralCheck, - (expectedCreditAccountAddress, 1, new uint256[](0), PERCENTAGE_FACTOR) + (expectedCreditAccountAddress, 1, new uint256[](0), PERCENTAGE_FACTOR, false) ) ); @@ -364,7 +363,6 @@ contract OpenCreditAccountIntegrationTest is IntegrationTestHelper, ICreditFacad AccountFactory(addressProvider.getAddressOrRevert(AP_ACCOUNT_FACTORY, NO_VERSION_CONTROL)).head(); uint256 blockAtOpen = block.number; - uint256 cumulativeAtOpen = pool.baseInterestIndex(); // pool.setCumulativeIndexNow(cumulativeAtOpen); MultiCall[] memory calls = MultiCallBuilder.build(); @@ -378,7 +376,6 @@ contract OpenCreditAccountIntegrationTest is IntegrationTestHelper, ICreditFacad (uint256 debt, uint256 cumulativeIndexLastUpdate,,,,,,) = creditManager.creditAccountInfo(creditAccount); assertEq(debt, 0, "Incorrect borrowed amount set in CA"); - assertEq(cumulativeIndexLastUpdate, cumulativeAtOpen, "Incorrect cumulativeIndexLastUpdate set in CA"); assertEq(ICreditAccount(creditAccount).since(), blockAtOpen, "Incorrect since set in CA"); diff --git a/contracts/test/integration/credit/Quotas.int.t.sol b/contracts/test/integration/credit/Quotas.int.t.sol index 8cb7d86c..ced523dd 100644 --- a/contracts/test/integration/credit/Quotas.int.t.sol +++ b/contracts/test/integration/credit/Quotas.int.t.sol @@ -8,7 +8,6 @@ import "../../../interfaces/ICreditFacadeV3Multicall.sol"; import { ICreditManagerV3, ICreditManagerV3Events, - ClosureAction, CollateralTokenData, ManageDebtAction, CollateralCalcTask, @@ -308,8 +307,50 @@ contract QuotasIntegrationTest is IntegrationTestHelper, ICreditManagerV3Events uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(pool)); - vm.prank(USER); - creditFacade.closeCreditAccount(creditAccount, USER, 0, false, new MultiCall[](0)); + vm.startPrank(USER); + creditFacade.closeCreditAccount( + creditAccount, + MultiCallBuilder.build( + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall( + ICreditFacadeV3Multicall.updateQuota, (tokenTestSuite.addressOf(Tokens.LINK), type(int96).min, 0) + ) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall( + ICreditFacadeV3Multicall.updateQuota, (tokenTestSuite.addressOf(Tokens.USDT), type(int96).min, 0) + ) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall(ICreditFacadeV3Multicall.decreaseDebt, (type(uint256).max)) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall( + ICreditFacadeV3Multicall.withdrawCollateral, (underlying, type(uint256).max, USER) + ) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall( + ICreditFacadeV3Multicall.withdrawCollateral, + (tokenTestSuite.addressOf(Tokens.LINK), type(uint256).max, USER) + ) + }), + MultiCall({ + target: address(creditFacade), + callData: abi.encodeCall( + ICreditFacadeV3Multicall.withdrawCollateral, + (tokenTestSuite.addressOf(Tokens.USDT), type(uint256).max, USER) + ) + }) + ) + ); + + vm.stopPrank(); expectBalance( Tokens.DAI, @@ -390,8 +431,8 @@ contract QuotasIntegrationTest is IntegrationTestHelper, ICreditManagerV3Events /// @dev I:[CMQ-08]: Credit Manager zeroes limits on quoted tokens upon incurring a loss function test_I_CMQ_08_creditManager_triggers_limit_zeroing_on_loss() public creditTest { - _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); - _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 500, uint96(1_000_000 * WAD)); + _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), type(uint16).max, uint96(1_000_000 * WAD)); + _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 500_00, uint96(1_000_000 * WAD)); (address creditAccount,) = _openTestCreditAccount(); @@ -400,7 +441,7 @@ contract QuotasIntegrationTest is IntegrationTestHelper, ICreditManagerV3Events target: address(creditFacade), callData: abi.encodeCall( ICreditFacadeV3Multicall.updateQuota, - (tokenTestSuite.addressOf(Tokens.LINK), int96(uint96(100 * WAD)), 0) + (tokenTestSuite.addressOf(Tokens.LINK), int96(uint96(10000000 * WAD)), 0) ) }), MultiCall({ @@ -421,7 +462,7 @@ contract QuotasIntegrationTest is IntegrationTestHelper, ICreditManagerV3Events address[2] memory quotedTokens = [tokenTestSuite.addressOf(Tokens.USDT), tokenTestSuite.addressOf(Tokens.LINK)]; vm.prank(USER); - creditFacade.closeCreditAccount(creditAccount, USER, 0, false, new MultiCall[](0)); + creditFacade.liquidateCreditAccount(creditAccount, FRIEND, new MultiCall[](0)); for (uint256 i = 0; i < quotedTokens.length; ++i) { (,,, uint96 limit,,) = poolQuotaKeeper.getTokenQuotaParams(quotedTokens[i]); diff --git a/contracts/test/integration/governance/GaugeMigration.int.t.sol b/contracts/test/integration/governance/GaugeMigration.int.t.sol index 94391ff1..fcc4fe37 100644 --- a/contracts/test/integration/governance/GaugeMigration.int.t.sol +++ b/contracts/test/integration/governance/GaugeMigration.int.t.sol @@ -171,8 +171,8 @@ contract GaugeMigrationIntegrationTest is Test { GearStakingV3 newStaking = new GearStakingV3(address(addressProvider), block.timestamp); GaugeV3 newGauge = new GaugeV3(address(pool), address(newStaking)); - staking.setSuccessor(address(newStaking)); newStaking.setMigrator(address(staking)); + staking.setSuccessor(address(newStaking)); staking.setVotingContractStatus(address(gauge), VotingContractStatus.UNVOTE_ONLY); newStaking.setVotingContractStatus(address(newGauge), VotingContractStatus.ALLOWED); diff --git a/contracts/test/interfaces/ICreditConfig.sol b/contracts/test/interfaces/ICreditConfig.sol index 1718b183..92c7adfc 100644 --- a/contracts/test/interfaces/ICreditConfig.sol +++ b/contracts/test/interfaces/ICreditConfig.sol @@ -13,6 +13,7 @@ struct PriceFeedConfig { address token; address priceFeed; uint32 stalenessPeriod; + bool trusted; } struct LinearIRMV3DeployParams { diff --git a/contracts/test/lib/MultiCallBuilder.sol b/contracts/test/lib/MultiCallBuilder.sol index a94e4d50..49d031ea 100644 --- a/contracts/test/lib/MultiCallBuilder.sol +++ b/contracts/test/lib/MultiCallBuilder.sol @@ -56,4 +56,21 @@ library MultiCallBuilder { calls[3] = call4; calls[4] = call5; } + + function build( + MultiCall memory call1, + MultiCall memory call2, + MultiCall memory call3, + MultiCall memory call4, + MultiCall memory call5, + MultiCall memory call6 + ) internal pure returns (MultiCall[] memory calls) { + calls = new MultiCall[](6); + calls[0] = call1; + calls[1] = call2; + calls[2] = call3; + calls[3] = call4; + calls[4] = call5; + calls[5] = call6; + } } diff --git a/contracts/test/mocks/core/AddressProviderV3ACLMock.sol b/contracts/test/mocks/core/AddressProviderV3ACLMock.sol index d64b228c..634163c1 100644 --- a/contracts/test/mocks/core/AddressProviderV3ACLMock.sol +++ b/contracts/test/mocks/core/AddressProviderV3ACLMock.sol @@ -6,7 +6,6 @@ pragma solidity ^0.8.17; import "../../../core/AddressProviderV3.sol"; import {AccountFactoryMock} from "../core/AccountFactoryMock.sol"; import {PriceOracleMock} from "../oracles/PriceOracleMock.sol"; -import {WithdrawalManagerMock} from "../core/WithdrawalManagerMock.sol"; import {BotListMock} from "../core/BotListMock.sol"; import {WETHMock} from "../token/WETHMock.sol"; @@ -31,9 +30,6 @@ contract AddressProviderV3ACLMock is Test, AddressProviderV3 { PriceOracleMock priceOracleMock = new PriceOracleMock(); _setAddress(AP_PRICE_ORACLE, address(priceOracleMock), priceOracleMock.version()); - WithdrawalManagerMock withdrawalManagerMock = new WithdrawalManagerMock(); - _setAddress(AP_WITHDRAWAL_MANAGER, address(withdrawalManagerMock), withdrawalManagerMock.version()); - AccountFactoryMock accountFactoryMock = new AccountFactoryMock(3_00); _setAddress(AP_ACCOUNT_FACTORY, address(accountFactoryMock), NO_VERSION_CONTROL); diff --git a/contracts/test/mocks/core/BotListMock.sol b/contracts/test/mocks/core/BotListMock.sol index d997293b..75a22cf0 100644 --- a/contracts/test/mocks/core/BotListMock.sol +++ b/contracts/test/mocks/core/BotListMock.sol @@ -38,15 +38,11 @@ contract BotListMock { revertOnErase = _value; } - function payBot(address payer, address creditManager, address creditAccount, address bot, uint72 paymentAmount) - external - {} - function setBotPermissionsReturn(uint256 activeBotsRemaining) external { return_activeBotsRemaining = activeBotsRemaining; } - function setBotPermissions(address, address, address, uint192, uint72, uint72) + function setBotPermissions(address, address, address, uint192) external view returns (uint256 activeBotsRemaining) diff --git a/contracts/test/mocks/core/WithdrawalManagerMock.sol b/contracts/test/mocks/core/WithdrawalManagerMock.sol deleted file mode 100644 index 04d71fe7..00000000 --- a/contracts/test/mocks/core/WithdrawalManagerMock.sol +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Foundation, 2023. -pragma solidity ^0.8.17; -pragma abicoder v1; - -import {ClaimAction} from "../../../interfaces/IWithdrawalManagerV3.sol"; - -struct CancellableWithdrawals { - address token; - uint256 amount; -} - -contract WithdrawalManagerMock { - uint256 public constant version = 3_00; - - uint40 public delay; - - mapping(bool => CancellableWithdrawals[2]) cancellableWithdrawals; - - bool public claimScheduledWithdrawalsWasCalled; - bool return_hasScheduled; - uint256 return_tokensToEnable; - - function cancellableScheduledWithdrawals(address, bool isForceCancel) - external - view - returns (address token1, uint256 amount1, address token2, uint256 amount2) - { - CancellableWithdrawals[2] storage cw = cancellableWithdrawals[isForceCancel]; - (token1, amount1) = (cw[0].token, cw[0].amount); - (token2, amount2) = (cw[1].token, cw[1].amount); - } - - function setCancellableWithdrawals( - bool isForceCancel, - address token1, - uint256 amount1, - address token2, - uint256 amount2 - ) external { - cancellableWithdrawals[isForceCancel][0] = CancellableWithdrawals({token: token1, amount: amount1}); - cancellableWithdrawals[isForceCancel][1] = CancellableWithdrawals({token: token2, amount: amount2}); - } - - function setDelay(uint40 _delay) external { - delay = _delay; - } - - function addScheduledWithdrawal(address creditAccount, address token, uint256 amount, uint8 tokenIndex) external {} - - function claimScheduledWithdrawals(address, address, ClaimAction) - external - returns (bool hasScheduled, uint256 tokensToEnable) - { - claimScheduledWithdrawalsWasCalled = true; - hasScheduled = return_hasScheduled; - tokensToEnable = return_tokensToEnable; - } - - function setClaimScheduledWithdrawals(bool hasScheduled, uint256 tokensToEnable) external { - return_hasScheduled = hasScheduled; - return_tokensToEnable = tokensToEnable; - } - - function addImmediateWithdrawal(address token, address to, uint256 amount) external {} - - function claimImmediateWithdrawal(address token, address to) external {} -} diff --git a/contracts/test/mocks/credit/CreditManagerMock.sol b/contracts/test/mocks/credit/CreditManagerMock.sol index 31a88fe4..661d6be1 100644 --- a/contracts/test/mocks/credit/CreditManagerMock.sol +++ b/contracts/test/mocks/credit/CreditManagerMock.sol @@ -7,7 +7,6 @@ import "../../../interfaces/IAddressProviderV3.sol"; import { ICreditManagerV3, - ClosureAction, CollateralDebtData, CollateralCalcTask, ManageDebtAction, @@ -15,8 +14,6 @@ import { } from "../../../interfaces/ICreditManagerV3.sol"; import {IPoolV3} from "../../../interfaces/IPoolV3.sol"; -import {ClaimAction} from "../../../interfaces/IWithdrawalManagerV3.sol"; - import "../../../interfaces/IExceptions.sol"; import "../../lib/constants.sol"; @@ -49,7 +46,7 @@ contract CreditManagerMock { CollateralDebtData return_collateralDebtData; - CollateralDebtData _closeCollateralDebtData; + CollateralDebtData _liquidateCollateralDebtData; uint256 internal _enabledTokensMask; address nextCreditAccount; @@ -85,8 +82,7 @@ contract CreditManagerMock { constructor(address _addressProvider, address _pool) { addressProvider = _addressProvider; - weth = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_WETH_TOKEN, NO_VERSION_CONTROL); // U:[CM-1] - withdrawalManager = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_WITHDRAWAL_MANAGER, 3_00); + weth = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_WETH_TOKEN, NO_VERSION_CONTROL); setPoolService(_pool); creditConfigurator = CONFIGURATOR; supportsQuotas = true; @@ -164,21 +160,18 @@ contract CreditManagerMock { return_loss = loss; } - function closeCreditAccount( - address, - ClosureAction, - CollateralDebtData memory collateralDebtData, - address, - address, - uint256, - bool - ) external returns (uint256 remainingFunds, uint256 loss) { - _closeCollateralDebtData = collateralDebtData; + function closeCreditAccount(address) external {} + + function liquidateCreditAccount(address, CollateralDebtData memory collateralDebtData, address, uint256, bool, bool) + external + returns (uint256 remainingFunds, uint256 loss) + { + _liquidateCollateralDebtData = collateralDebtData; remainingFunds = return_remainingFunds; loss = return_loss; } - function fullCollateralCheck(address, uint256 enabledTokensMask, uint256[] memory, uint16) + function fullCollateralCheck(address, uint256 enabledTokensMask, uint256[] memory, uint16, bool) external pure returns (uint256) @@ -206,16 +199,8 @@ contract CreditManagerMock { return_collateralDebtData = _collateralDebtData; } - function closeCollateralDebtData() external view returns (CollateralDebtData memory) { - return _closeCollateralDebtData; - } - - function setClaimWithdrawals(uint256 tokensToEnable) external { - cw_return_tokensToEnable = tokensToEnable; - } - - function claimWithdrawals(address, address, ClaimAction) external view returns (uint256 tokensToEnable) { - tokensToEnable = cw_return_tokensToEnable; + function liquidateCollateralDebtData() external view returns (CollateralDebtData memory) { + return _liquidateCollateralDebtData; } function enabledTokensMaskOf(address) external view returns (uint256) { @@ -237,8 +222,7 @@ contract CreditManagerMock { /// @notice Returns the mask containing miscellaneous account flags /// @dev Currently, the following flags are supported: - /// * 1 - WITHDRAWALS_FLAG - whether the account has pending withdrawals - /// * 2 - BOT_PERMISSIONS_FLAG - whether the account has non-zero permissions for at least one bot + /// * 1 - BOT_PERMISSIONS_FLAG - whether the account has non-zero permissions for at least one bot function flagsOf(address) external view returns (uint16) { return flags; // U:[CM-35] } @@ -289,11 +273,11 @@ contract CreditManagerMock { tokensToDisable = md_return_tokensToDisable; } - function setScheduleWithdrawal(uint256 tokensToDisable) external { + function setWithdrawCollateral(uint256 tokensToDisable) external { sw_tokensToDisable = tokensToDisable; } - function scheduleWithdrawal(address, address, uint256) external view returns (uint256 tokensToDisable) { + function withdrawCollateral(address, address, uint256, address) external view returns (uint256 tokensToDisable) { tokensToDisable = sw_tokensToDisable; } diff --git a/contracts/test/mocks/oracles/PriceOracleMock.sol b/contracts/test/mocks/oracles/PriceOracleMock.sol index db185bae..cee1f661 100644 --- a/contracts/test/mocks/oracles/PriceOracleMock.sol +++ b/contracts/test/mocks/oracles/PriceOracleMock.sol @@ -18,12 +18,20 @@ contract PriceOracleMock is Test, IPriceOracleBase { uint256 public constant override version = 3_00; mapping(address => bool) revertsOnGetPrice; - mapping(address => address) public priceFeeds; + mapping(address => mapping(bool => address)) priceFeedsInt; constructor() { vm.label(address(this), "PRICE_ORACLE"); } + function priceFeeds(address token) public view returns (address) { + return priceFeedsInt[token][false]; + } + + function priceFeedsRaw(address token, bool reserve) public view returns (address) { + return priceFeedsInt[token][reserve]; + } + function setRevertOnGetPrice(address token, bool value) external { revertsOnGetPrice[token] = value; } @@ -33,7 +41,7 @@ contract PriceOracleMock is Test, IPriceOracleBase { } function addPriceFeed(address token, address priceFeed) external { - priceFeeds[token] = priceFeed; + priceFeedsInt[token][false] = priceFeed; } /// @dev Converts a quantity of an asset to USD (decimals = 8). @@ -74,7 +82,7 @@ contract PriceOracleMock is Test, IPriceOracleBase { /// @dev Returns the price feed address for the passed token /// @param token Token to get the price feed for function priceFeedsOrRevert(address token) external view returns (address priceFeed) { - priceFeed = priceFeeds[token]; + priceFeed = priceFeedsInt[token][false]; require(priceFeed != address(0), "Price feed is not set"); } } diff --git a/contracts/test/suites/GenesisFactory.sol b/contracts/test/suites/GenesisFactory.sol index fb3cbf66..f50b79b3 100644 --- a/contracts/test/suites/GenesisFactory.sol +++ b/contracts/test/suites/GenesisFactory.sol @@ -14,7 +14,6 @@ import {GearStakingV3} from "../../governance/GearStakingV3.sol"; import {PriceFeedConfig} from "../interfaces/ICreditConfig.sol"; import "../../interfaces/IAddressProviderV3.sol"; -import {WithdrawalManagerV3} from "../../core/WithdrawalManagerV3.sol"; import {BotListV3} from "../../core/BotListV3.sol"; import {PriceOracleV3} from "../../core/PriceOracleV3.sol"; import {GearToken} from "@gearbox-protocol/core-v2/contracts/tokens/GearToken.sol"; @@ -49,9 +48,6 @@ contract GenesisFactory is Ownable { addressProvider.setAddress(AP_ACCOUNT_FACTORY, accountFactory, false); - WithdrawalManagerV3 wm = new WithdrawalManagerV3(address(addressProvider), 1 days); - addressProvider.setAddress(AP_WITHDRAWAL_MANAGER, address(wm), true); - BotListV3 botList = new BotListV3(address(addressProvider)); addressProvider.setAddress(AP_BOT_LIST, address(botList), true); @@ -68,7 +64,9 @@ contract GenesisFactory is Ownable { function addPriceFeeds(PriceFeedConfig[] memory priceFeeds) external onlyOwner { uint256 len = priceFeeds.length; for (uint256 i; i < len; ++i) { - priceOracle.setPriceFeed(priceFeeds[i].token, priceFeeds[i].priceFeed, priceFeeds[i].stalenessPeriod); + priceOracle.setPriceFeed( + priceFeeds[i].token, priceFeeds[i].priceFeed, priceFeeds[i].stalenessPeriod, priceFeeds[i].trusted + ); } acl.transferOwnership(msg.sender); } diff --git a/contracts/test/suites/TokensTestSuite.sol b/contracts/test/suites/TokensTestSuite.sol index bd29e1ab..92de8353 100644 --- a/contracts/test/suites/TokensTestSuite.sol +++ b/contracts/test/suites/TokensTestSuite.sol @@ -110,7 +110,9 @@ contract TokensTestSuite is Test, TokensTestSuiteHelper { tokenIndexes[address(t)] = token.index; - priceFeeds.push(PriceFeedConfig({token: address(t), priceFeed: priceFeed, stalenessPeriod: 2 hours})); + priceFeeds.push( + PriceFeedConfig({token: address(t), priceFeed: priceFeed, stalenessPeriod: 2 hours, trusted: true}) + ); symbols[token.index] = token.symbol; priceFeedsMap[token.index] = priceFeed; } diff --git a/contracts/test/unit/core/BotList.unit.t.sol b/contracts/test/unit/core/BotList.unit.t.sol deleted file mode 100644 index 2a583ecb..00000000 --- a/contracts/test/unit/core/BotList.unit.t.sol +++ /dev/null @@ -1,572 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Foundation, 2023. -pragma solidity ^0.8.17; - -import {BotListV3} from "../../../core/BotListV3.sol"; -import {IBotListV3Events, BotFunding} from "../../../interfaces/IBotListV3.sol"; -import {ICreditAccountBase} from "../../../interfaces/ICreditAccountV3.sol"; -import {ICreditManagerV3} from "../../../interfaces/ICreditManagerV3.sol"; -import {ICreditFacadeV3} from "../../../interfaces/ICreditFacadeV3.sol"; - -// TEST -import "../../lib/constants.sol"; - -// MOCKS -import {AddressProviderV3ACLMock, AP_WETH_TOKEN, AP_TREASURY} from "../../mocks/core/AddressProviderV3ACLMock.sol"; -import {WETHMock} from "../../mocks/token/WETHMock.sol"; -import {GeneralMock} from "../../mocks/GeneralMock.sol"; - -// SUITES -import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; - -// EXCEPTIONS -import "../../../interfaces/IExceptions.sol"; - -contract InvalidCFMock { - address public creditManager; - - constructor(address _creditManager) { - creditManager = _creditManager; - } -} - -/// @title LPPriceFeedTest -/// @notice Designed for unit test purposes only -contract BotListTest is Test, IBotListV3Events { - AddressProviderV3ACLMock public addressProvider; - WETHMock public weth; - - BotListV3 botList; - - TokensTestSuite tokenTestSuite; - - GeneralMock bot; - address creditManager; - address creditFacade; - address creditAccount; - - address invalidCF; - - function setUp() public { - vm.prank(CONFIGURATOR); - addressProvider = new AddressProviderV3ACLMock(); - weth = WETHMock(payable(addressProvider.getAddressOrRevert(AP_WETH_TOKEN, 0))); - - tokenTestSuite = new TokensTestSuite(); - - botList = new BotListV3(address(addressProvider)); - - bot = new GeneralMock(); - creditManager = makeAddr("CREDIT_MANAGER"); - creditFacade = makeAddr("CREDIT_FACADE"); - creditAccount = makeAddr("CREDIT_ACCOUNT"); - - invalidCF = address(new InvalidCFMock(address(creditManager))); - - vm.mockCall( - address(creditManager), - abi.encodeWithSelector(ICreditManagerV3.creditFacade.selector), - abi.encode(address(creditFacade)) - ); - - vm.mockCall( - address(creditFacade), - abi.encodeWithSelector(ICreditFacadeV3.creditManager.selector), - abi.encode(address(creditManager)) - ); - - vm.mockCall( - creditAccount, - abi.encodeWithSelector(ICreditAccountBase.creditManager.selector), - abi.encode(address(creditManager)) - ); - - vm.prank(CONFIGURATOR); - botList.setApprovedCreditManagerStatus(address(creditManager), true); - } - - /// - /// - /// TESTS - /// - /// - - /// @dev [BL-1]: constructor sets correct values - function test_U_BL_01_constructor_sets_correct_values() public { - assertEq(botList.treasury(), addressProvider.getAddressOrRevert(AP_TREASURY, 0), "Treasury contract incorrect"); - assertEq(botList.weth(), address(weth), "WETH incorrect"); - assertEq(botList.daoFee(), 0, "Initial DAO fee incorrect"); - } - - /// @dev [BL-2]: setDAOFee works correctly - function test_U_BL_02_setDAOFee_works_correctly() public { - vm.expectRevert(CallerNotConfiguratorException.selector); - botList.setDAOFee(1); - - vm.expectEmit(false, false, false, true); - emit SetBotDAOFee(15); - - vm.prank(CONFIGURATOR); - botList.setDAOFee(15); - - assertEq(botList.daoFee(), 15, "DAO fee incorrect"); - } - - /// @dev [BL-3]: setBotPermissions works correctly - function test_U_BL_03_setBotPermissions_works_correctly() public { - vm.expectRevert(CallerNotCreditFacadeException.selector); - vm.prank(invalidCF); - botList.setBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - permissions: type(uint192).max, - totalFundingAllowance: uint72(1 ether), - weeklyFundingAllowance: uint72(1 ether / 10) - }); - - vm.expectRevert(abi.encodeWithSelector(AddressIsNotContractException.selector, DUMB_ADDRESS)); - vm.prank(address(creditFacade)); - botList.setBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: DUMB_ADDRESS, - permissions: type(uint192).max, - totalFundingAllowance: uint72(1 ether), - weeklyFundingAllowance: uint72(1 ether / 10) - }); - - vm.prank(CONFIGURATOR); - botList.setBotForbiddenStatus(address(creditManager), address(bot), true); - - vm.expectRevert(InvalidBotException.selector); - vm.prank(address(creditFacade)); - botList.setBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - permissions: type(uint192).max, - totalFundingAllowance: uint72(1 ether), - weeklyFundingAllowance: uint72(1 ether / 10) - }); - - vm.prank(CONFIGURATOR); - botList.setBotForbiddenStatus(address(creditManager), address(bot), false); - - vm.prank(CONFIGURATOR); - botList.setBotSpecialPermissions(address(creditManager), address(bot), 1); - - vm.expectRevert(InvalidBotException.selector); - vm.prank(address(creditFacade)); - botList.setBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - permissions: type(uint192).max, - totalFundingAllowance: uint72(1 ether), - weeklyFundingAllowance: uint72(1 ether / 10) - }); - - vm.prank(CONFIGURATOR); - botList.setBotSpecialPermissions(address(creditManager), address(bot), 0); - - vm.expectEmit(true, true, true, true); - emit SetBotPermissions(creditManager, creditAccount, address(bot), 1, uint72(1 ether), uint72(1 ether / 10)); - - vm.prank(address(creditFacade)); - uint256 activeBotsRemaining = botList.setBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - permissions: 1, - totalFundingAllowance: uint72(1 ether), - weeklyFundingAllowance: uint72(1 ether / 10) - }); - - assertEq(activeBotsRemaining, 1, "Incorrect number of bots returned"); - - assertEq(botList.botPermissions(creditManager, creditAccount, address(bot)), 1, "Bot permissions were not set"); - - (uint256 remainingFunds, uint256 maxWeeklyAllowance, uint256 remainingWeeklyAllowance, uint256 allowanceLU) = - botList.botFunding(creditManager, creditAccount, address(bot)); - - address[] memory bots = botList.getActiveBots(creditManager, creditAccount); - - assertEq(bots.length, 1, "Incorrect active bots array length"); - - assertEq(bots[0], address(bot), "Incorrect address added to active bots list"); - - assertEq(remainingFunds, 1 ether, "Incorrect remaining funds value"); - - assertEq(maxWeeklyAllowance, 1 ether / 10, "Incorrect max weekly allowance"); - - assertEq(remainingWeeklyAllowance, 1 ether / 10, "Incorrect remaining weekly allowance"); - - assertEq(allowanceLU, block.timestamp, "Incorrect allowance update timestamp"); - - vm.prank(address(creditFacade)); - activeBotsRemaining = botList.setBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - permissions: 2, - totalFundingAllowance: uint72(2 ether), - weeklyFundingAllowance: uint72(2 ether / 10) - }); - - (remainingFunds, maxWeeklyAllowance, remainingWeeklyAllowance, allowanceLU) = - botList.botFunding(creditManager, creditAccount, address(bot)); - - assertEq(activeBotsRemaining, 1, "Incorrect number of bots returned"); - - assertEq(botList.botPermissions(creditManager, creditAccount, address(bot)), 2, "Bot permissions were not set"); - - assertEq(remainingFunds, 2 ether, "Incorrect remaining funds value"); - - assertEq(maxWeeklyAllowance, 2 ether / 10, "Incorrect max weekly allowance"); - - assertEq(remainingWeeklyAllowance, 2 ether / 10, "Incorrect remaining weekly allowance"); - - assertEq(allowanceLU, block.timestamp, "Incorrect allowance update timestamp"); - - bots = botList.getActiveBots(creditManager, creditAccount); - - assertEq(bots.length, 1, "Incorrect active bots array length"); - - assertEq(bots[0], address(bot), "Incorrect address added to active bots list"); - - vm.prank(CONFIGURATOR); - botList.setBotForbiddenStatus(address(creditManager), address(bot), true); - - vm.expectEmit(true, true, true, false); - emit EraseBot(address(creditManager), address(creditAccount), address(bot)); - - vm.prank(address(creditFacade)); - activeBotsRemaining = botList.setBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - permissions: 0, - totalFundingAllowance: 0, - weeklyFundingAllowance: 0 - }); - - (remainingFunds, maxWeeklyAllowance, remainingWeeklyAllowance, allowanceLU) = - botList.botFunding(creditManager, creditAccount, address(bot)); - - assertEq(activeBotsRemaining, 0, "Incorrect number of bots returned"); - - assertEq(botList.botPermissions(creditManager, creditAccount, address(bot)), 0, "Bot permissions were not set"); - - assertEq(remainingFunds, 0, "Incorrect remaining funds value"); - - assertEq(maxWeeklyAllowance, 0, "Incorrect max weekly allowance"); - - assertEq(remainingWeeklyAllowance, 0, "Incorrect remaining weekly allowance"); - - assertEq(allowanceLU, 0, "Incorrect allowance update timestamp"); - - bots = botList.getActiveBots(creditManager, creditAccount); - - assertEq(bots.length, 0, "Incorrect active bots array length"); - } - - /// @dev [BL-4]: deposit and withdraw work correctly - function test_U_BL_04_deposit_withdraw_work_correctly() public { - vm.deal(USER, 10 ether); - - vm.expectRevert(AmountCantBeZeroException.selector); - botList.deposit(); - - vm.expectEmit(true, false, false, true); - emit Deposit(USER, 1 ether); - - vm.prank(USER); - botList.deposit{value: 1 ether}(); - - assertEq(botList.balanceOf(USER), 1 ether, "User's bot funding wallet has incorrect balance"); - - vm.expectEmit(true, false, false, true); - emit Deposit(USER, 1 ether); - - vm.prank(USER); - botList.deposit{value: 1 ether}(); - - vm.expectRevert(AmountCantBeZeroException.selector); - vm.prank(USER); - botList.withdraw(0); - - uint256 userBalance = botList.balanceOf(USER); - - vm.expectRevert(InsufficientBalanceException.selector); - vm.prank(USER); - botList.withdraw(userBalance + 1); - - assertEq(botList.balanceOf(USER), 2 ether, "User's bot funding wallet has incorrect balance"); - - vm.expectEmit(true, false, false, true); - emit Withdraw(USER, 1 ether / 2); - - vm.prank(USER); - botList.withdraw(1 ether / 2); - - assertEq(botList.balanceOf(USER), 3 ether / 2, "User's bot funding wallet has incorrect balance"); - - assertEq(USER.balance, 85 ether / 10, "User's balance is incorrect"); - } - - /// @dev [BL-5]: payBot works correctly - function test_U_BL_05_payBot_works_correctly() public { - vm.prank(CONFIGURATOR); - botList.setDAOFee(5000); - - vm.mockCall( - address(creditManager), - abi.encodeWithSelector(ICreditManagerV3.getBorrowerOrRevert.selector, creditAccount), - abi.encode(USER) - ); - - vm.deal(USER, 10 ether); - - vm.prank(USER); - botList.deposit{value: 2 ether}(); - - vm.prank(address(creditFacade)); - botList.setBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - permissions: 1, - totalFundingAllowance: uint72(1 ether), - weeklyFundingAllowance: uint72(1 ether / 10) - }); - - vm.warp(block.timestamp + 1 days); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - vm.prank(invalidCF); - botList.payBot({ - payer: USER, - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - paymentAmount: uint72(1 ether / 20) - }); - - vm.expectEmit(true, true, true, true); - emit PayBot(USER, creditAccount, address(bot), uint72(1 ether / 20), uint72(1 ether / 40)); - - vm.prank(address(creditFacade)); - botList.payBot({ - payer: USER, - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - paymentAmount: uint72(1 ether / 20) - }); - - (uint256 remainingFunds, uint256 maxWeeklyAllowance, uint256 remainingWeeklyAllowance, uint256 allowanceLU) = - botList.botFunding(creditManager, creditAccount, address(bot)); - - assertEq(remainingFunds, 1 ether - (1 ether / 20) - (1 ether / 40), "Bot funding remaining funds incorrect"); - - assertEq( - remainingWeeklyAllowance, - (1 ether / 10) - (1 ether / 20) - (1 ether / 40), - "Bot remaining weekly allowance incorrect" - ); - - assertEq( - botList.balanceOf(USER), - 2 ether - (1 ether / 20) - (1 ether / 40), - "User remaining funding balance incorrect" - ); - - assertEq(allowanceLU, block.timestamp - 1 days, "Allowance update timestamp incorrect"); - - assertEq(weth.balanceOf(address(bot)), 1 ether / 20, "Bot was sent incorrect WETH amount"); - - assertEq(botList.collectedDaoFees(), 1 ether / 40, "Collected dao fees was increased with incorrect amount"); - - vm.warp(block.timestamp + 7 days); - - vm.prank(address(creditFacade)); - botList.payBot({ - payer: USER, - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - paymentAmount: uint72(1 ether / 20) - }); - - (remainingFunds, maxWeeklyAllowance, remainingWeeklyAllowance, allowanceLU) = - botList.botFunding(creditManager, creditAccount, address(bot)); - - assertEq(remainingFunds, 1 ether - (2 ether / 20) - (2 ether / 40), "Bot funding remaining funds incorrect"); - - assertEq( - remainingWeeklyAllowance, - (1 ether / 10) - (1 ether / 20) - (1 ether / 40), - "Bot remaining weekly allowance incorrect" - ); - - assertEq(allowanceLU, block.timestamp, "Allowance update timestamp incorrect"); - - assertEq( - botList.balanceOf(USER), - 2 ether - (2 ether / 20) - (2 ether / 40), - "User remaining funding balance incorrect" - ); - - assertEq(weth.balanceOf(address(bot)), 2 ether / 20, "Bot was sent incorrect WETH amount"); - - assertEq(botList.collectedDaoFees(), 2 ether / 40, "CollectedDaoFees was incorrectly chaged"); - - botList.transferCollectedDaoFees(); - - assertEq(botList.collectedDaoFees(), 0, "CollectedDaoFees was not zeroed"); - - assertEq( - weth.balanceOf(addressProvider.getTreasuryContract()), 2 ether / 40, "Treasury was sent incorrect amount" - ); - } - - /// @dev [BL-6]: eraseAllBotPermissions works correctly - function test_U_BL_06_eraseAllBotPermissions_works_correctly() public { - vm.prank(address(creditFacade)); - botList.setBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - permissions: 1, - totalFundingAllowance: uint72(1 ether), - weeklyFundingAllowance: uint72(1 ether / 10) - }); - - address bot2 = address(new GeneralMock()); - - vm.prank(address(creditFacade)); - uint256 activeBotsRemaining = botList.setBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot2), - permissions: 2, - totalFundingAllowance: uint72(2 ether), - weeklyFundingAllowance: uint72(2 ether / 10) - }); - - assertEq(activeBotsRemaining, 2, "Incorrect number of active bots"); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - vm.prank(invalidCF); - botList.eraseAllBotPermissions(creditManager, creditAccount); - - // it starts removing bots from the end - vm.expectEmit(true, true, true, false); - emit EraseBot(creditManager, creditAccount, address(bot2)); - - vm.expectEmit(true, true, true, false); - emit EraseBot(creditManager, creditAccount, address(bot)); - - vm.prank(address(creditFacade)); - botList.eraseAllBotPermissions(creditManager, creditAccount); - - assertEq( - botList.botPermissions(creditManager, creditAccount, address(bot)), - 0, - "Permissions were not erased for bot 1" - ); - - assertEq( - botList.botPermissions(creditManager, creditAccount, address(bot2)), - 0, - "Permissions were not erased for bot 2" - ); - - (uint256 remainingFunds, uint256 maxWeeklyAllowance, uint256 remainingWeeklyAllowance, uint256 allowanceLU) = - botList.botFunding(creditManager, creditAccount, address(bot)); - - assertEq(remainingFunds, 0, "Remaining funds were not zeroed"); - - assertEq(maxWeeklyAllowance, 0, "Remaining funds were not zeroed"); - - assertEq(remainingWeeklyAllowance, 0, "Remaining funds were not zeroed"); - - (remainingFunds, maxWeeklyAllowance, remainingWeeklyAllowance, allowanceLU) = - botList.botFunding(creditManager, creditAccount, address(bot2)); - - assertEq(remainingFunds, 0, "Remaining funds were not zeroed"); - - assertEq(maxWeeklyAllowance, 0, "Remaining funds were not zeroed"); - - assertEq(remainingWeeklyAllowance, 0, "Remaining funds were not zeroed"); - - address[] memory activeBots = botList.getActiveBots(creditManager, creditAccount); - - assertEq(activeBots.length, 0, "Not all active bots were disabled"); - } - - /// @dev [BL-7]: setBotSpecialPermissions works correctly - function test_U_BL_07_setBotSpecialPermissions_works_correctly() public { - vm.expectRevert(CallerNotConfiguratorException.selector); - botList.setBotSpecialPermissions(address(creditManager), address(bot), 2); - - vm.expectEmit(true, true, false, true); - emit SetBotSpecialPermissions(address(creditManager), address(bot), 2); - - vm.prank(CONFIGURATOR); - botList.setBotSpecialPermissions(address(creditManager), address(bot), 2); - - (uint192 permissions,, bool hasSpecialPermissions) = - botList.getBotStatus(address(creditManager), address(creditAccount), address(bot)); - - assertEq(permissions, 2, "Special permissions are incorrect"); - - assertTrue(hasSpecialPermissions, "Special permissions status returned incorrectly"); - } - - /// @dev [BL-8]: payBot correctly reverts if payment bigger than allowances - function test_U_BL_08_payBot_correctly_reverts_if_payment_bigger_than_allowances() public { - uint72 limit = 1 ether; - vm.prank(address(creditFacade)); - botList.setBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - permissions: 1, - totalFundingAllowance: type(uint72).max, - weeklyFundingAllowance: limit - }); - - vm.expectRevert(InsufficientWeeklyFundingAllowance.selector); - vm.prank(address(creditFacade)); - botList.payBot({ - payer: USER, - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - paymentAmount: limit + 1 - }); - - vm.prank(address(creditFacade)); - botList.setBotPermissions({ - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - permissions: 1, - totalFundingAllowance: limit, - weeklyFundingAllowance: type(uint72).max - }); - - vm.expectRevert(InsufficientTotalFundingAllowance.selector); - vm.prank(address(creditFacade)); - botList.payBot({ - payer: USER, - creditManager: creditManager, - creditAccount: creditAccount, - bot: address(bot), - paymentAmount: limit + 1 - }); - } -} diff --git a/contracts/test/unit/core/BotListV3.unit.t.sol b/contracts/test/unit/core/BotListV3.unit.t.sol new file mode 100644 index 00000000..a68298ef --- /dev/null +++ b/contracts/test/unit/core/BotListV3.unit.t.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2023. +pragma solidity ^0.8.17; + +import {BotListV3} from "../../../core/BotListV3.sol"; +import {IBotListV3Events} from "../../../interfaces/IBotListV3.sol"; + +// TEST +import "../../lib/constants.sol"; + +// MOCKS +import {AddressProviderV3ACLMock} from "../../mocks/core/AddressProviderV3ACLMock.sol"; + +// EXCEPTIONS +import "../../../interfaces/IExceptions.sol"; + +/// @title Bot list V3 unit test +/// @notice U:[BL]: Unit tests for bot list v3 +contract BotListV3UnitTest is Test, IBotListV3Events { + BotListV3 botList; + + AddressProviderV3ACLMock addressProvider; + + address bot; + address otherBot; + address creditManager; + address creditFacade; + address creditAccount; + address invalidFacade; + + function setUp() public { + bot = makeAddr("BOT"); + otherBot = makeAddr("OTHER_BOT"); + creditManager = makeAddr("CREDIT_MANAGER"); + creditFacade = makeAddr("CREDIT_FACADE"); + creditAccount = makeAddr("CREDIT_ACCOUNT"); + invalidFacade = makeAddr("INVALID_FACADE"); + + vm.etch(bot, "RANDOM CODE"); + vm.etch(otherBot, "RANDOM CODE"); + vm.mockCall(creditManager, abi.encodeWithSignature("creditFacade()"), abi.encode(creditFacade)); + vm.mockCall(creditFacade, abi.encodeWithSignature("creditManager()"), abi.encode(creditManager)); + vm.mockCall(invalidFacade, abi.encodeWithSignature("creditManager()"), abi.encode(creditManager)); + vm.mockCall(creditAccount, abi.encodeWithSignature("creditManager()"), abi.encode(creditManager)); + + vm.startPrank(CONFIGURATOR); + addressProvider = new AddressProviderV3ACLMock(); + addressProvider.addCreditManager(creditManager); + botList = new BotListV3(address(addressProvider)); + botList.setCreditManagerApprovedStatus(creditManager, true); + vm.stopPrank(); + } + + /// @notice U:[BL-1]: `setBotPermissions` works correctly + function test_U_BL_01_setBotPermissions_works_correctly() public { + vm.expectRevert(CallerNotCreditFacadeException.selector); + vm.prank(invalidFacade); + botList.setBotPermissions({ + bot: bot, + creditManager: creditManager, + creditAccount: creditAccount, + permissions: type(uint192).max + }); + + vm.expectRevert(abi.encodeWithSelector(AddressIsNotContractException.selector, DUMB_ADDRESS)); + vm.prank(creditFacade); + botList.setBotPermissions({ + bot: DUMB_ADDRESS, + creditManager: creditManager, + creditAccount: creditAccount, + permissions: type(uint192).max + }); + + vm.prank(CONFIGURATOR); + botList.setBotForbiddenStatus(bot, true); + + vm.expectRevert(InvalidBotException.selector); + vm.prank(creditFacade); + botList.setBotPermissions({ + bot: bot, + creditManager: creditManager, + creditAccount: creditAccount, + permissions: type(uint192).max + }); + + vm.prank(CONFIGURATOR); + botList.setBotForbiddenStatus(bot, false); + + vm.prank(CONFIGURATOR); + botList.setBotSpecialPermissions(bot, creditManager, 1); + + vm.expectRevert(InvalidBotException.selector); + vm.prank(creditFacade); + botList.setBotPermissions({ + bot: bot, + creditManager: creditManager, + creditAccount: creditAccount, + permissions: type(uint192).max + }); + + vm.prank(CONFIGURATOR); + botList.setBotSpecialPermissions(bot, creditManager, 0); + + vm.expectEmit(true, true, true, true); + emit SetBotPermissions(bot, creditManager, creditAccount, 1); + + vm.prank(creditFacade); + uint256 activeBotsRemaining = botList.setBotPermissions({ + bot: bot, + creditManager: creditManager, + creditAccount: creditAccount, + permissions: 1 + }); + + assertEq(activeBotsRemaining, 1, "Incorrect number of bots returned"); + assertEq(botList.botPermissions(bot, creditManager, creditAccount), 1, "Bot permissions were not set"); + + address[] memory bots = botList.activeBots(creditManager, creditAccount); + assertEq(bots.length, 1, "Incorrect active bots array length"); + assertEq(bots[0], bot, "Incorrect address added to active bots list"); + + vm.prank(creditFacade); + activeBotsRemaining = botList.setBotPermissions({ + bot: bot, + creditManager: creditManager, + creditAccount: creditAccount, + permissions: 2 + }); + + assertEq(activeBotsRemaining, 1, "Incorrect number of bots returned"); + assertEq(botList.botPermissions(bot, creditManager, creditAccount), 2, "Bot permissions were not set"); + + bots = botList.activeBots(creditManager, creditAccount); + assertEq(bots.length, 1, "Incorrect active bots array length"); + assertEq(bots[0], bot, "Incorrect address added to active bots list"); + + vm.prank(CONFIGURATOR); + botList.setBotForbiddenStatus(bot, true); + + vm.expectEmit(true, true, true, false); + emit EraseBot(bot, creditManager, creditAccount); + + vm.prank(creditFacade); + activeBotsRemaining = botList.setBotPermissions({ + bot: bot, + creditManager: creditManager, + creditAccount: creditAccount, + permissions: 0 + }); + + assertEq(activeBotsRemaining, 0, "Incorrect number of bots returned"); + assertEq(botList.botPermissions(bot, creditManager, creditAccount), 0, "Bot permissions were not set"); + + bots = botList.activeBots(creditManager, creditAccount); + assertEq(bots.length, 0, "Incorrect active bots array length"); + } + + /// @dev U:[BL-2]: `eraseAllBotPermissions` works correctly + function test_U_BL_02_eraseAllBotPermissions_works_correctly() public { + vm.prank(creditFacade); + botList.setBotPermissions({bot: bot, creditManager: creditManager, creditAccount: creditAccount, permissions: 1}); + + vm.prank(creditFacade); + uint256 activeBotsRemaining = botList.setBotPermissions({ + bot: otherBot, + creditManager: creditManager, + creditAccount: creditAccount, + permissions: 2 + }); + + assertEq(activeBotsRemaining, 2, "Incorrect number of active bots"); + + vm.expectRevert(CallerNotCreditFacadeException.selector); + vm.prank(invalidFacade); + botList.eraseAllBotPermissions(creditManager, creditAccount); + + vm.expectEmit(true, true, true, false); + emit EraseBot(otherBot, creditManager, creditAccount); + + vm.expectEmit(true, true, true, false); + emit EraseBot(bot, creditManager, creditAccount); + + vm.prank(creditFacade); + botList.eraseAllBotPermissions(creditManager, creditAccount); + + assertEq(botList.botPermissions(bot, creditManager, creditAccount), 0, "Permissions not erased for bot 1"); + assertEq(botList.botPermissions(otherBot, creditManager, creditAccount), 0, "Permissions not erased for bot 2"); + + address[] memory activeBots = botList.activeBots(creditManager, creditAccount); + assertEq(activeBots.length, 0, "Not all active bots were disabled"); + } + + /// @dev U:[BL-3]: `setBotSpecialPermissions` works correctly + function test_U_BL_03_setBotSpecialPermissions_works_correctly() public { + vm.expectRevert(CallerNotConfiguratorException.selector); + botList.setBotSpecialPermissions(bot, creditManager, 2); + + vm.expectEmit(true, true, false, true); + emit SetBotSpecialPermissions(bot, creditManager, 2); + + vm.prank(CONFIGURATOR); + botList.setBotSpecialPermissions(bot, creditManager, 2); + + (uint192 permissions,, bool hasSpecialPermissions) = botList.getBotStatus(bot, creditManager, creditAccount); + + assertEq(permissions, 2, "Special permissions are incorrect"); + assertTrue(hasSpecialPermissions, "Special permissions status returned incorrectly"); + } +} diff --git a/contracts/test/unit/core/PriceOracleV3.unit.t.sol b/contracts/test/unit/core/PriceOracleV3.unit.t.sol index 3c9355ce..26a66caa 100644 --- a/contracts/test/unit/core/PriceOracleV3.unit.t.sol +++ b/contracts/test/unit/core/PriceOracleV3.unit.t.sol @@ -83,13 +83,20 @@ contract PriceOracleV3UnitTest is Test, IPriceOracleV3Events { { priceOracle.hackPriceFeedParams(token, expectedParams); + if (expectedParams.priceFeed == address(0)) { + vm.expectRevert(PriceFeedDoesNotExistException.selector); + } + PriceFeedParams memory params = priceOracle.getPriceFeedParams(token); + if (expectedParams.priceFeed == address(0)) return; + assertEq(params.priceFeed, expectedParams.priceFeed, "Incorrect priceFeed"); assertEq(params.stalenessPeriod, expectedParams.stalenessPeriod, "Incorrect stalenessPeriod"); assertEq(params.decimals, expectedParams.decimals, "Incorrect decimals"); assertEq(params.skipCheck, expectedParams.skipCheck, "Incorrect skipCheck"); assertEq(params.useReserve, expectedParams.useReserve, "Incorrect useReserve"); + assertEq(params.trusted, expectedParams.trusted, "Incorrect trusted"); } /// @notice U:[PO-3]: `_getTokenReserveKey` works as expected @@ -205,19 +212,19 @@ contract PriceOracleV3UnitTest is Test, IPriceOracleV3Events { PriceFeedMock priceFeed = new PriceFeedMock(42, 8); vm.expectRevert(ZeroAddressException.selector); - priceOracle.setPriceFeed(address(0), address(priceFeed), 0); + priceOracle.setPriceFeed(address(0), address(priceFeed), 0, false); vm.expectRevert(ZeroAddressException.selector); - priceOracle.setPriceFeed(address(token), address(0), 0); + priceOracle.setPriceFeed(address(token), address(0), 0, false); vm.expectRevert(CallerNotConfiguratorException.selector); - priceOracle.setPriceFeed(address(token), address(priceFeed), 0); + priceOracle.setPriceFeed(address(token), address(priceFeed), 0, false); vm.expectEmit(true, true, false, true); - emit SetPriceFeed(address(token), address(priceFeed), 3600, false); + emit SetPriceFeed(address(token), address(priceFeed), 3600, false, false); vm.prank(configurator); - priceOracle.setPriceFeed(address(token), address(priceFeed), 3600); + priceOracle.setPriceFeed(address(token), address(priceFeed), 3600, false); PriceFeedParams memory params = priceOracle.getPriceFeedParams(address(token)); assertEq(params.priceFeed, address(priceFeed), "Incorrect priceFeed"); assertEq(params.decimals, 18, "Incorrect decimals"); @@ -232,19 +239,19 @@ contract PriceOracleV3UnitTest is Test, IPriceOracleV3Events { PriceFeedMock priceFeed = new PriceFeedMock(42, 8); vm.expectRevert(ZeroAddressException.selector); - priceOracle.setPriceFeed(address(0), address(priceFeed), 0); + priceOracle.setPriceFeed(address(0), address(priceFeed), 0, false); vm.expectRevert(ZeroAddressException.selector); - priceOracle.setPriceFeed(address(token), address(0), 0); + priceOracle.setPriceFeed(address(token), address(0), 0, false); vm.expectRevert(CallerNotConfiguratorException.selector); - priceOracle.setPriceFeed(address(token), address(priceFeed), 0); + priceOracle.setPriceFeed(address(token), address(priceFeed), 0, false); vm.expectRevert(PriceFeedDoesNotExistException.selector); vm.prank(configurator); priceOracle.setReservePriceFeed(address(token), address(priceFeed), 0); - priceOracle.hackPriceFeedParams(address(token), PriceFeedParams(address(0), 0, false, 18, false)); + priceOracle.hackPriceFeedParams(address(token), PriceFeedParams(address(0), 0, false, 18, false, false)); priceFeed.setSkipPriceCheck(FlagState.TRUE); vm.expectEmit(true, true, false, true); @@ -273,8 +280,12 @@ contract PriceOracleV3UnitTest is Test, IPriceOracleV3Events { vm.prank(configurator); priceOracle.setReservePriceFeedStatus(address(token), true); - priceOracle.hackPriceFeedParams(address(token), PriceFeedParams(address(0), 0, false, 18, false)); - priceOracle.hackReservePriceFeedParams(address(token), PriceFeedParams(address(priceFeed), 0, false, 18, false)); + priceOracle.hackPriceFeedParams( + address(token), PriceFeedParams(makeAddr("MAIN_PRICE_FEED"), 0, false, 18, false, false) + ); + priceOracle.hackReservePriceFeedParams( + address(token), PriceFeedParams(address(priceFeed), 0, false, 18, false, false) + ); vm.expectEmit(true, false, false, true); emit SetReservePriceFeedStatus(address(token), true); @@ -295,7 +306,7 @@ contract PriceOracleV3UnitTest is Test, IPriceOracleV3Events { function test_U_PO_09_covnertToUSD_and_convertFromUSD_work_as_expected() public { ERC20Mock token = new ERC20Mock("Test Token", "TEST", 6); PriceFeedMock priceFeed = new PriceFeedMock(2e8, 8); - priceOracle.hackPriceFeedParams(address(token), PriceFeedParams(address(priceFeed), 0, false, 6, false)); + priceOracle.hackPriceFeedParams(address(token), PriceFeedParams(address(priceFeed), 0, false, 6, false, false)); assertEq(priceOracle.convertToUSD(100e6, address(token)), 200e8, "Incorrect convertToUSD"); assertEq(priceOracle.convertFromUSD(1000e8, address(token)), 500e6, "Incorrect convertFromUSD"); @@ -305,11 +316,15 @@ contract PriceOracleV3UnitTest is Test, IPriceOracleV3Events { function test_U_PO_10_convert_works_as_expected() public { ERC20Mock token1 = new ERC20Mock("Test Token 1", "TEST1", 6); PriceFeedMock priceFeed1 = new PriceFeedMock(10e8, 8); - priceOracle.hackPriceFeedParams(address(token1), PriceFeedParams(address(priceFeed1), 0, false, 6, false)); + priceOracle.hackPriceFeedParams( + address(token1), PriceFeedParams(address(priceFeed1), 0, false, 6, false, false) + ); ERC20Mock token2 = new ERC20Mock("Test Token 2", "TEST2", 18); PriceFeedMock priceFeed2 = new PriceFeedMock(0.1e8, 8); - priceOracle.hackPriceFeedParams(address(token2), PriceFeedParams(address(priceFeed2), 0, false, 18, false)); + priceOracle.hackPriceFeedParams( + address(token2), PriceFeedParams(address(priceFeed2), 0, false, 18, false, false) + ); assertEq( priceOracle.convert(1e6, address(token1), address(token2)), 100e18, "Incorrect token1 -> token2 conversion" @@ -319,4 +334,36 @@ contract PriceOracleV3UnitTest is Test, IPriceOracleV3Events { priceOracle.convert(100e18, address(token2), address(token1)), 1e6, "Incorrect token2 -> token1 conversion" ); } + + /// @notice U:[PO-11]: Safe conversion works as expected + function test_U_PO_11_safe_conversion_works_as_expected() public { + ERC20Mock token = new ERC20Mock("Test Token", "TEST", 6); + PriceFeedMock mainFeed = new PriceFeedMock(2e8, 8); + PriceFeedMock reserveFeed = new PriceFeedMock(1.8e8, 8); + + // untrusted feed without reserve feed + priceOracle.hackPriceFeedParams(address(token), PriceFeedParams(address(mainFeed), 0, false, 6, false, false)); + assertEq(priceOracle.getPriceSafe(address(token)), 0, "getPriceSafe, untrusted feed without reserve feed"); + assertEq( + priceOracle.safeConvertToUSD(100e6, address(token)), + 0, + "safeConvertToUSD untrusted feed without reserve feed" + ); + + // untrusted feed with reserve feed + priceOracle.hackReservePriceFeedParams( + address(token), PriceFeedParams(address(reserveFeed), 0, false, 6, false, false) + ); + assertEq(priceOracle.getPriceSafe(address(token)), 1.8e8, "safe price of untrusted feed with reserve feed"); + assertEq( + priceOracle.safeConvertToUSD(100e6, address(token)), + 180e8, + "safeConvertToUSD untrusted feed with reserve feed" + ); + + // trusted feed + priceOracle.hackPriceFeedParams(address(token), PriceFeedParams(address(mainFeed), 0, false, 6, false, true)); + assertEq(priceOracle.getPriceSafe(address(token)), 2e8, "safe price of trusted feed"); + assertEq(priceOracle.safeConvertToUSD(100e6, address(token)), 200e8, "safeConvertToUSD trusted feed"); + } } diff --git a/contracts/test/unit/core/PriceOracleV3Harness.sol b/contracts/test/unit/core/PriceOracleV3Harness.sol index 02380be2..b448ff45 100644 --- a/contracts/test/unit/core/PriceOracleV3Harness.sol +++ b/contracts/test/unit/core/PriceOracleV3Harness.sol @@ -13,15 +13,15 @@ contract PriceOracleV3Harness is PriceOracleV3 { } function getPriceFeedParams(address token) external view returns (PriceFeedParams memory) { - (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals, bool useReserve) = + (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals, bool useReserve, bool trusted) = _getPriceFeedParams(token); - return PriceFeedParams(priceFeed, stalenessPeriod, skipCheck, decimals, useReserve); + return PriceFeedParams(priceFeed, stalenessPeriod, skipCheck, decimals, useReserve, trusted); } function getReservePriceFeedParams(address token) external view returns (PriceFeedParams memory) { - (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals, bool useReserve) = + (address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals, bool useReserve, bool trusted) = _getPriceFeedParams(_getTokenReserveKey(token)); - return PriceFeedParams(priceFeed, stalenessPeriod, skipCheck, decimals, useReserve); + return PriceFeedParams(priceFeed, stalenessPeriod, skipCheck, decimals, useReserve, trusted); } function getPrice(address priceFeed, uint32 stalenessPeriod, bool skipCheck, uint8 decimals) diff --git a/contracts/test/unit/core/WithdrawalManagerV3.unit.t.sol b/contracts/test/unit/core/WithdrawalManagerV3.unit.t.sol deleted file mode 100644 index 1ff9099b..00000000 --- a/contracts/test/unit/core/WithdrawalManagerV3.unit.t.sol +++ /dev/null @@ -1,754 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Foundation, 2023. -pragma solidity ^0.8.17; - -import { - ClaimAction, - ETH_ADDRESS, - IWithdrawalManagerV3Events, - ScheduledWithdrawal -} from "../../../interfaces/IWithdrawalManagerV3.sol"; -import { - AmountCantBeZeroException, - CallerNotConfiguratorException, - CallerNotCreditManagerException, - NoFreeWithdrawalSlotsException, - NothingToClaimException, - ReceiveIsNotAllowedException, - RegisteredCreditManagerOnlyException, - ZeroAddressException -} from "../../../interfaces/IExceptions.sol"; - -import {Tokens} from "@gearbox-protocol/sdk-gov/contracts/Tokens.sol"; -import {USER} from "../../lib/constants.sol"; -import {TestHelper} from "../../lib/helper.sol"; -import {AddressProviderV3ACLMock, AP_WETH_TOKEN} from "../../mocks/core/AddressProviderV3ACLMock.sol"; -import {ERC20Mock} from "../../mocks/token/ERC20Mock.sol"; -import {ERC20BlacklistableMock} from "../../mocks/token/ERC20Blacklistable.sol"; -import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; - -import {WithdrawalManagerV3Harness} from "./WithdrawalManagerV3Harness.sol"; - -enum ScheduleTask { - IMMATURE, - MATURE, - NON_SCHEDULED -} - -/// @title Withdrawal manager V3 unit test -/// @notice U:[WM]: Unit tests for withdrawal manager -contract WithdrawalManagerV3UnitTest is TestHelper, IWithdrawalManagerV3Events { - WithdrawalManagerV3Harness manager; - AddressProviderV3ACLMock acl; - TokensTestSuite ts; - ERC20BlacklistableMock token0; - ERC20Mock token1; - - address configurator; - address creditAccount; - address creditManager; - - uint40 constant DELAY = 1 days; - uint256 constant AMOUNT = 10 ether; - uint8 constant TOKEN0_INDEX = 0; - uint256 constant TOKEN0_MASK = 1; - uint8 constant TOKEN1_INDEX = 1; - uint256 constant TOKEN1_MASK = 2; - - function setUp() public { - configurator = makeAddr("CONFIGURATOR"); - creditAccount = makeAddr("CREDIT_ACCOUNT"); - creditManager = makeAddr("CREDIT_MANAGER"); - - ts = new TokensTestSuite(); - ts.topUpWETH{value: AMOUNT}(); - token0 = ERC20BlacklistableMock(ts.addressOf(Tokens.USDC)); - token1 = ERC20Mock(ts.addressOf(Tokens.WETH)); - - vm.startPrank(configurator); - acl = new AddressProviderV3ACLMock(); - acl.setAddress(AP_WETH_TOKEN, address(token1), false); - acl.addCreditManager(creditManager); - manager = new WithdrawalManagerV3Harness(address(acl), DELAY); - manager.addCreditManager(creditManager); - vm.stopPrank(); - } - - // ------------- // - // GENERAL TESTS // - // ------------- // - - /// @notice U:[WM-1]: Constructor sets correct values - function test_U_WM_01_constructor_sets_correct_values() public { - assertEq(manager.delay(), DELAY, "Incorrect delay"); - } - - /// @notice U:[WM-2]: External functions have correct access - function test_U_WM_02_external_functions_have_correct_access() public { - vm.startPrank(USER); - - deal(USER, 1 ether); - vm.expectRevert(ReceiveIsNotAllowedException.selector); - payable(manager).transfer(1 ether); - - vm.expectRevert(CallerNotCreditManagerException.selector); - manager.addImmediateWithdrawal(address(0), address(0), 0); - - vm.expectRevert(CallerNotCreditManagerException.selector); - manager.addScheduledWithdrawal(address(0), address(0), 0, 0); - - vm.expectRevert(CallerNotCreditManagerException.selector); - manager.claimScheduledWithdrawals(address(0), address(0), ClaimAction(0)); - - vm.expectRevert(CallerNotConfiguratorException.selector); - manager.setWithdrawalDelay(0); - - vm.expectRevert(CallerNotConfiguratorException.selector); - manager.addCreditManager(address(0)); - - vm.stopPrank(); - } - - // --------------------------- // - // IMMEDIATE WITHDRAWALS TESTS // - // --------------------------- // - - /// @notice U:[WM-3]: `addImmediateWithdrawal` works correctly - function test_U_WM_03_addImmediateWithdrawal_works_correctly() public { - vm.startPrank(creditManager); - - // add first withdrawal - deal(address(token0), address(manager), AMOUNT); - - vm.expectEmit(true, true, false, true); - emit AddImmediateWithdrawal(USER, address(token0), AMOUNT); - - manager.addImmediateWithdrawal(address(token0), USER, AMOUNT); - - assertEq( - manager.immediateWithdrawals(USER, address(token0)), - AMOUNT, - "Incorrect claimable balance after adding first withdrawal" - ); - - // add second withdrawal in the same token - deal(address(token0), address(manager), AMOUNT); - - vm.expectEmit(true, true, false, true); - emit AddImmediateWithdrawal(USER, address(token0), AMOUNT); - - manager.addImmediateWithdrawal(address(token0), USER, AMOUNT); - - assertEq( - manager.immediateWithdrawals(USER, address(token0)), - 2 * AMOUNT, - "Incorrect claimable balance after adding second withdrawal" - ); - - vm.stopPrank(); - } - - /// @notice U:[WM-4A]: `claimImmediateWithdrawal` reverts on zero recipient - function test_U_WM_04A_claimImmediateWithdrawal_reverts_on_zero_recipient() public { - vm.expectRevert(ZeroAddressException.selector); - vm.prank(USER); - manager.claimImmediateWithdrawal(address(token0), address(0)); - } - - /// @notice U:[WM-4C]: `claimImmediateWithdrawal` works correctly - function test_U_WM_04C_claimImmediateWithdrawal_works_correctly() public { - deal(address(token0), address(manager), AMOUNT); - - vm.prank(creditManager); - manager.addImmediateWithdrawal(address(token0), USER, AMOUNT); - - vm.expectEmit(true, true, false, true); - emit ClaimImmediateWithdrawal(USER, address(token0), USER, AMOUNT - 1); - - vm.prank(USER); - manager.claimImmediateWithdrawal(address(token0), USER); - - assertEq(manager.immediateWithdrawals(USER, address(token0)), 1, "Incorrect claimable balance"); - assertEq(ts.balanceOf(Tokens.USDC, USER), AMOUNT - 1, "Incorrect claimed amount"); - } - - /// @notice U:[WM-4D]: `claimImmediateWithdrawal` works correctly with Ether - function test_U_WM_04D_claimImmediateWithdrawal_works_correctly_with_ether() public { - deal(address(token1), address(manager), AMOUNT); - - vm.prank(creditManager); - manager.addImmediateWithdrawal(address(token1), USER, AMOUNT); - - vm.expectEmit(true, true, false, true); - emit ClaimImmediateWithdrawal(USER, address(token1), USER, AMOUNT - 1); - - vm.prank(USER); - manager.claimImmediateWithdrawal(ETH_ADDRESS, USER); - - assertEq(manager.immediateWithdrawals(USER, address(token1)), 1, "Incorrect claimable balance"); - assertEq(address(USER).balance, AMOUNT - 1, "Incorrect claimed amount"); - } - - // ----------------------------------------------- // - // SCHEDULED WITHDRAWALS: EXTERNAL FUNCTIONS TESTS // - // ----------------------------------------------- // - - /// @notice U:[WM-5A]: `addScheduledWithdrawal` reverts on zero amount - function test_U_WM_05A_addScheduledWithdrawal_reverts_on_zero_amount() public { - vm.expectRevert(AmountCantBeZeroException.selector); - vm.prank(creditManager); - manager.addScheduledWithdrawal(creditAccount, address(token0), 1, TOKEN0_INDEX); - } - - struct AddScheduledWithdrawalCase { - string name; - // scenario - ScheduleTask task0; - ScheduleTask task1; - // expected result - bool shouldRevert; - uint8 expectedSlot; - } - - /// @notice U:[WM-5B]: `addScheduledWithdrawal` works correctly - function test_U_WM_05B_addScheduledWithdrawal_works_correctly() public { - AddScheduledWithdrawalCase[4] memory cases = [ - AddScheduledWithdrawalCase({ - name: "both slots non-scheduled", - task0: ScheduleTask.NON_SCHEDULED, - task1: ScheduleTask.NON_SCHEDULED, - shouldRevert: false, - expectedSlot: 0 - }), - AddScheduledWithdrawalCase({ - name: "slot 0 non-scheduled, slot 1 scheduled", - task0: ScheduleTask.NON_SCHEDULED, - task1: ScheduleTask.MATURE, - shouldRevert: false, - expectedSlot: 0 - }), - AddScheduledWithdrawalCase({ - name: "slot 0 scheduled, slot 1 non-scheduled", - task0: ScheduleTask.MATURE, - task1: ScheduleTask.NON_SCHEDULED, - shouldRevert: false, - expectedSlot: 1 - }), - AddScheduledWithdrawalCase({ - name: "both slots scheduled", - task0: ScheduleTask.MATURE, - task1: ScheduleTask.MATURE, - shouldRevert: true, - expectedSlot: 0 - }) - ]; - - uint256 snapshot = vm.snapshot(); - for (uint256 i; i < cases.length; ++i) { - _addScheduledWithdrawal({slot: 0, task: cases[i].task0}); - _addScheduledWithdrawal({slot: 1, task: cases[i].task1}); - - uint40 expectedMaturity = uint40(block.timestamp) + DELAY; - - if (cases[i].shouldRevert) { - vm.expectRevert(NoFreeWithdrawalSlotsException.selector); - } else { - vm.expectEmit(true, true, false, true); - emit AddScheduledWithdrawal(creditAccount, address(token0), AMOUNT, expectedMaturity); - } - - vm.prank(creditManager); - manager.addScheduledWithdrawal(creditAccount, address(token0), AMOUNT, TOKEN0_INDEX); - - if (!cases[i].shouldRevert) { - ScheduledWithdrawal memory w = manager.scheduledWithdrawals(creditAccount)[cases[i].expectedSlot]; - assertEq(w.tokenIndex, TOKEN0_INDEX, _testCaseErr(cases[i].name, "incorrect token index")); - assertEq(w.token, address(token0), _testCaseErr(cases[i].name, "incorrect token")); - assertEq(w.maturity, expectedMaturity, _testCaseErr(cases[i].name, "incorrect maturity")); - assertEq(w.amount, AMOUNT, _testCaseErr(cases[i].name, "incorrect amount")); - } - - vm.revertTo(snapshot); - } - } - - /// @notice U:[WM-6A]: `claimScheduledWithdrawals` reverts on nothing to claim when action is `CLAIM` - function test_U_WM_06A_claimScheduledWithdrawals_reverts_on_nothing_to_claim() public { - _addScheduledWithdrawal({slot: 0, task: ScheduleTask.IMMATURE}); - _addScheduledWithdrawal({slot: 1, task: ScheduleTask.NON_SCHEDULED}); - vm.expectRevert(NothingToClaimException.selector); - vm.prank(creditManager); - manager.claimScheduledWithdrawals(creditAccount, USER, ClaimAction.CLAIM); - } - - struct ClaimScheduledWithdrawalsCase { - string name; - // scenario - ClaimAction action; - ScheduleTask task0; - ScheduleTask task1; - // expected result - bool shouldClaim0; - bool shouldClaim1; - bool shouldCancel0; - bool shouldCancel1; - bool expectedHasScheduled; - uint256 expectedTokensToEnable; - } - - /// @notice U:[WM-6B]: `claimScheduledWithdrawals` works correctly - function test_U_WM_06B_claimScheduledWithdrawals_works_correctly() public { - ClaimScheduledWithdrawalsCase[5] memory cases = [ - ClaimScheduledWithdrawalsCase({ - name: "action == CLAIM, slot 0 mature, slot 1 immature", - action: ClaimAction.CLAIM, - task0: ScheduleTask.MATURE, - task1: ScheduleTask.IMMATURE, - shouldClaim0: true, - shouldClaim1: false, - shouldCancel0: false, - shouldCancel1: false, - expectedHasScheduled: true, - expectedTokensToEnable: 0 - }), - ClaimScheduledWithdrawalsCase({ - name: "action == CLAIM, slot 0 mature, slot 1 non-scheduled", - action: ClaimAction.CLAIM, - task0: ScheduleTask.MATURE, - task1: ScheduleTask.NON_SCHEDULED, - shouldClaim0: true, - shouldClaim1: false, - shouldCancel0: false, - shouldCancel1: false, - expectedHasScheduled: false, - expectedTokensToEnable: 0 - }), - ClaimScheduledWithdrawalsCase({ - name: "action == CANCEL, slot 0 mature, slot 1 immature", - action: ClaimAction.CANCEL, - task0: ScheduleTask.MATURE, - task1: ScheduleTask.IMMATURE, - shouldClaim0: true, - shouldClaim1: false, - shouldCancel0: false, - shouldCancel1: true, - expectedHasScheduled: false, - expectedTokensToEnable: TOKEN1_MASK - }), - ClaimScheduledWithdrawalsCase({ - name: "action == FORCE_CLAIM, slot 0 mature, slot 1 immature", - action: ClaimAction.FORCE_CLAIM, - task0: ScheduleTask.MATURE, - task1: ScheduleTask.IMMATURE, - shouldClaim0: true, - shouldClaim1: true, - shouldCancel0: false, - shouldCancel1: false, - expectedHasScheduled: false, - expectedTokensToEnable: 0 - }), - ClaimScheduledWithdrawalsCase({ - name: "action == FORCE_CANCEL, slot 0 mature, slot 1 immature", - action: ClaimAction.FORCE_CANCEL, - task0: ScheduleTask.MATURE, - task1: ScheduleTask.IMMATURE, - shouldClaim0: false, - shouldClaim1: false, - shouldCancel0: true, - shouldCancel1: true, - expectedHasScheduled: false, - expectedTokensToEnable: TOKEN0_MASK | TOKEN1_MASK - }) - ]; - - uint256 snapshot = vm.snapshot(); - for (uint256 i; i < cases.length; ++i) { - _addScheduledWithdrawal({slot: 0, task: cases[i].task0}); - _addScheduledWithdrawal({slot: 1, task: cases[i].task1}); - - if (cases[i].shouldClaim0) { - vm.expectEmit(true, true, false, false); - emit ClaimScheduledWithdrawal(creditAccount, address(token0), address(0), 0); - } - if (cases[i].shouldClaim1) { - vm.expectEmit(true, true, false, false); - emit ClaimScheduledWithdrawal(creditAccount, address(token1), address(0), 0); - } - if (cases[i].shouldCancel0) { - vm.expectEmit(true, true, false, false); - emit CancelScheduledWithdrawal(creditAccount, address(token0), 0); - } - if (cases[i].shouldCancel1) { - vm.expectEmit(true, true, false, false); - emit CancelScheduledWithdrawal(creditAccount, address(token1), 0); - } - - vm.prank(creditManager); - (bool hasScheduled, uint256 tokensToEnable) = - manager.claimScheduledWithdrawals(creditAccount, USER, cases[i].action); - - assertEq(hasScheduled, cases[i].expectedHasScheduled, _testCaseErr(cases[i].name, "incorrect hasScheduled")); - assertEq( - tokensToEnable, cases[i].expectedTokensToEnable, _testCaseErr(cases[i].name, "incorrect tokensToEnable") - ); - - vm.revertTo(snapshot); - } - } - - struct CancellableScheduledWithdrawalsCase { - string name; - // scenario - bool isForceCancel; - ScheduleTask task0; - ScheduleTask task1; - // expected results - address expectedToken0; - uint256 expectedAmount0; - address expectedToken1; - uint256 expectedAmount1; - } - - /// @notice U:[WM-7]: `cancellableScheduledWithdrawals` works correctly - function test_U_WM_07_cancellableScheduledWithdrawals_works_correctly() public { - CancellableScheduledWithdrawalsCase[4] memory cases = [ - CancellableScheduledWithdrawalsCase({ - name: "cancel, both slots mature", - isForceCancel: false, - task0: ScheduleTask.MATURE, - task1: ScheduleTask.MATURE, - expectedToken0: address(0), - expectedAmount0: 0, - expectedToken1: address(0), - expectedAmount1: 0 - }), - CancellableScheduledWithdrawalsCase({ - name: "cancel, slot 0 immature, slot 1 mature", - isForceCancel: false, - task0: ScheduleTask.IMMATURE, - task1: ScheduleTask.MATURE, - expectedToken0: address(token0), - expectedAmount0: AMOUNT - 1, - expectedToken1: address(0), - expectedAmount1: 0 - }), - CancellableScheduledWithdrawalsCase({ - name: "force cancel, slot 0 immature, slot 1 mature", - isForceCancel: true, - task0: ScheduleTask.IMMATURE, - task1: ScheduleTask.MATURE, - expectedToken0: address(token0), - expectedAmount0: AMOUNT - 1, - expectedToken1: address(token1), - expectedAmount1: AMOUNT - 1 - }), - CancellableScheduledWithdrawalsCase({ - name: "force cancel, both slots mature", - isForceCancel: true, - task0: ScheduleTask.MATURE, - task1: ScheduleTask.MATURE, - expectedToken0: address(token0), - expectedAmount0: AMOUNT - 1, - expectedToken1: address(token1), - expectedAmount1: AMOUNT - 1 - }) - ]; - - uint256 snapshot = vm.snapshot(); - for (uint256 i; i < cases.length; ++i) { - _addScheduledWithdrawal({slot: 0, task: cases[i].task0}); - _addScheduledWithdrawal({slot: 1, task: cases[i].task1}); - - (address token0_, uint256 amount0, address token1_, uint256 amount1) = - manager.cancellableScheduledWithdrawals(creditAccount, cases[i].isForceCancel); - - assertEq(token0_, cases[i].expectedToken0, _testCaseErr(cases[i].name, "incorrect token0")); - assertEq(amount0, cases[i].expectedAmount0, _testCaseErr(cases[i].name, "incorrect amount0")); - assertEq(token1_, cases[i].expectedToken1, _testCaseErr(cases[i].name, "incorrect token0")); - assertEq(amount1, cases[i].expectedAmount1, _testCaseErr(cases[i].name, "incorrect amount1")); - - vm.revertTo(snapshot); - } - } - - // ----------------------------------------------- // - // SCHEDULED WITHDRAWALS: INTERNAL FUNCTIONS TESTS // - // ----------------------------------------------- // - - struct ProcessScheduledWithdrawalCase { - string name; - // scenario - ClaimAction action; - ScheduleTask task; - // expected result - bool shouldClaim; - bool shouldCancel; - bool expectedScheduled; - bool expectedClaimed; - uint256 expectedTokensToEnable; - } - - /// @notice U:[WM-8]: `_processScheduledWithdrawal` works correctly - function test_U_WM_08_processScheduledWithdrawal_works_correctly() public { - ProcessScheduledWithdrawalCase[12] memory cases = [ - ProcessScheduledWithdrawalCase({ - name: "immature withdrawal, action == CLAIM", - action: ClaimAction.CLAIM, - task: ScheduleTask.IMMATURE, - shouldClaim: false, - shouldCancel: false, - expectedScheduled: true, - expectedClaimed: false, - expectedTokensToEnable: 0 - }), - ProcessScheduledWithdrawalCase({ - name: "immature withdrawal, action == CANCEL", - action: ClaimAction.CANCEL, - task: ScheduleTask.IMMATURE, - shouldClaim: false, - shouldCancel: true, - expectedScheduled: false, - expectedClaimed: false, - expectedTokensToEnable: TOKEN0_MASK - }), - ProcessScheduledWithdrawalCase({ - name: "immature withdrawal, action == FORCE_CLAIM", - action: ClaimAction.FORCE_CLAIM, - task: ScheduleTask.IMMATURE, - shouldClaim: true, - shouldCancel: false, - expectedScheduled: false, - expectedClaimed: true, - expectedTokensToEnable: 0 - }), - ProcessScheduledWithdrawalCase({ - name: "immature withdrawal, action == FORCE_CANCEL", - action: ClaimAction.FORCE_CANCEL, - task: ScheduleTask.IMMATURE, - shouldClaim: false, - shouldCancel: true, - expectedScheduled: false, - expectedClaimed: false, - expectedTokensToEnable: TOKEN0_MASK - }), - ProcessScheduledWithdrawalCase({ - name: "mature withdrawal, action == CLAIM", - action: ClaimAction.CLAIM, - task: ScheduleTask.MATURE, - shouldClaim: true, - shouldCancel: false, - expectedScheduled: false, - expectedClaimed: true, - expectedTokensToEnable: 0 - }), - ProcessScheduledWithdrawalCase({ - name: "mature withdrawal, action == CANCEL", - action: ClaimAction.CANCEL, - task: ScheduleTask.MATURE, - shouldClaim: true, - shouldCancel: false, - expectedScheduled: false, - expectedClaimed: true, - expectedTokensToEnable: 0 - }), - ProcessScheduledWithdrawalCase({ - name: "mature withdrawal, action == FORCE_CLAIM", - action: ClaimAction.FORCE_CLAIM, - task: ScheduleTask.MATURE, - shouldClaim: true, - shouldCancel: false, - expectedScheduled: false, - expectedClaimed: true, - expectedTokensToEnable: 0 - }), - ProcessScheduledWithdrawalCase({ - name: "mature withdrawal, action == FORCE_CANCEL", - action: ClaimAction.FORCE_CANCEL, - task: ScheduleTask.MATURE, - shouldClaim: false, - shouldCancel: true, - expectedScheduled: false, - expectedClaimed: false, - expectedTokensToEnable: TOKEN0_MASK - }), - // - ProcessScheduledWithdrawalCase({ - name: "non-scheduled withdrawal, action == CLAIM", - action: ClaimAction.CLAIM, - task: ScheduleTask.NON_SCHEDULED, - shouldClaim: false, - shouldCancel: false, - expectedScheduled: false, - expectedClaimed: false, - expectedTokensToEnable: 0 - }), - ProcessScheduledWithdrawalCase({ - name: "non-scheduled withdrawal, action == CANCEL", - action: ClaimAction.CANCEL, - task: ScheduleTask.NON_SCHEDULED, - shouldClaim: false, - shouldCancel: false, - expectedScheduled: false, - expectedClaimed: false, - expectedTokensToEnable: 0 - }), - ProcessScheduledWithdrawalCase({ - name: "non-scheduled withdrawal, action == FORCE_CLAIM", - action: ClaimAction.FORCE_CLAIM, - task: ScheduleTask.NON_SCHEDULED, - shouldClaim: false, - shouldCancel: false, - expectedScheduled: false, - expectedClaimed: false, - expectedTokensToEnable: 0 - }), - ProcessScheduledWithdrawalCase({ - name: "non-scheduled withdrawal, action == FORCE_CANCEL", - action: ClaimAction.FORCE_CANCEL, - task: ScheduleTask.NON_SCHEDULED, - shouldClaim: false, - shouldCancel: false, - expectedScheduled: false, - expectedClaimed: false, - expectedTokensToEnable: 0 - }) - ]; - - uint256 snapshot = vm.snapshot(); - for (uint256 i; i < cases.length; ++i) { - _addScheduledWithdrawal({slot: 0, task: cases[i].task}); - - if (cases[i].shouldClaim) { - vm.expectEmit(true, true, false, false); - emit ClaimScheduledWithdrawal(creditAccount, address(token0), address(0), 0); - } - if (cases[i].shouldCancel) { - vm.expectEmit(true, true, false, false); - emit CancelScheduledWithdrawal(creditAccount, address(token0), 0); - } - - (bool scheduled, bool claimed, uint256 tokensToEnable) = manager.processScheduledWithdrawal({ - creditAccount: creditAccount, - slot: 0, - action: cases[i].action, - to: USER - }); - - assertEq(scheduled, cases[i].expectedScheduled, _testCaseErr(cases[i].name, "incorrect scheduled")); - assertEq(claimed, cases[i].expectedClaimed, _testCaseErr(cases[i].name, "incorrect claimed")); - assertEq( - tokensToEnable, cases[i].expectedTokensToEnable, _testCaseErr(cases[i].name, "incorrect tokensToEnable") - ); - - vm.revertTo(snapshot); - } - } - - /// @notice U:[WM-9A]: `_claimScheduledWithdrawal` works correctly - function test_U_WM_09A_claimScheduledWithdrawal_works_correctly() public { - _addScheduledWithdrawal({slot: 0, task: ScheduleTask.MATURE}); - - vm.expectEmit(true, true, false, true); - emit ClaimScheduledWithdrawal(creditAccount, address(token0), USER, AMOUNT - 1); - - manager.claimScheduledWithdrawal({creditAccount: creditAccount, slot: 0, to: USER}); - - assertEq(token0.balanceOf(address(manager)), 1, "Incorrect manager balance"); - assertEq(token0.balanceOf(USER), AMOUNT - 1, "Incorrect recipient balance"); - - ScheduledWithdrawal memory w = manager.scheduledWithdrawals(creditAccount)[0]; - assertEq(w.maturity, 1, "Withdrawal not cleared"); - } - - /// @notice U:[WM-9B]: `_claimScheduledWithdrawal` works correctly with blacklisted recipient - function test_U_WM_09B_claimScheduledWithdrawal_works_correctly_with_blacklisted_recipient() public { - _addScheduledWithdrawal({slot: 0, task: ScheduleTask.MATURE}); - token0.setBlacklisted(USER, true); - - vm.expectEmit(true, true, false, true); - emit ClaimScheduledWithdrawal(creditAccount, address(token0), USER, AMOUNT - 1); - - vm.expectEmit(true, true, false, true); - emit AddImmediateWithdrawal(USER, address(token0), AMOUNT - 1); - - manager.claimScheduledWithdrawal({creditAccount: creditAccount, slot: 0, to: USER}); - - assertEq(token0.balanceOf(address(manager)), AMOUNT, "Incorrect manager balance"); - - ScheduledWithdrawal memory w = manager.scheduledWithdrawals(creditAccount)[0]; - assertEq(w.maturity, 1, "Withdrawal not cleared"); - } - - /// @notice U:[WM-10]: `_cancelScheduledWithdrawal` works correctly - function test_U_WM_10_cancelScheduledWithdrawal_works_correctly() public { - _addScheduledWithdrawal({slot: 0, task: ScheduleTask.MATURE}); - - vm.expectEmit(true, true, false, true); - emit CancelScheduledWithdrawal(creditAccount, address(token0), AMOUNT - 1); - - uint256 tokensToEnable = manager.cancelScheduledWithdrawal({creditAccount: creditAccount, slot: 0}); - - assertEq(token0.balanceOf(address(manager)), 1, "Incorrect manager balance"); - assertEq(token0.balanceOf(creditAccount), AMOUNT - 1, "Incorrect credit account balance"); - assertEq(tokensToEnable, TOKEN0_MASK, "Incorrect tokensToEnable"); - } - - // ------------------- // - // CONFIGURATION TESTS // - // ------------------- // - - /// @notice U:[WM-11]: `setWithdrawalDelay` works correctly - function test_U_WM_11_setWithdrawalDelay_works_correctly() public { - uint40 newDelay = 2 days; - - vm.expectEmit(false, false, false, true); - emit SetWithdrawalDelay(newDelay); - - vm.prank(configurator); - manager.setWithdrawalDelay(newDelay); - - assertEq(manager.delay(), newDelay, "Incorrect delay"); - } - - /// @notice U:[WM-12A]: `addCreditManager` reverts for non-registered credit manager - function test_U_WM_12A_addCreditManager_reverts_for_non_registered_credit_manager() public { - vm.expectRevert(RegisteredCreditManagerOnlyException.selector); - vm.prank(configurator); - manager.addCreditManager(address(0)); - } - - /// @notice U:[WM-12B]: `addCreditManager` works correctly - function test_U_WM_12B_addCreditManager_works_correctly() public { - manager = new WithdrawalManagerV3Harness(address(acl), DELAY); - - vm.expectEmit(true, false, false, false); - emit AddCreditManager(creditManager); - - vm.prank(configurator); - manager.addCreditManager(creditManager); - - assertTrue(manager.isValidCreditManager(creditManager), "Credit Manager status was not set"); - } - - // ------- // - // HELPERS // - // ------- // - - function _addScheduledWithdrawal(uint8 slot, ScheduleTask task) internal { - ScheduledWithdrawal memory withdrawal; - if (task == ScheduleTask.NON_SCHEDULED) { - withdrawal.amount = 1; - withdrawal.maturity = 1; - } else { - address token = slot == 0 ? address(token0) : address(token1); - deal(token, address(manager), AMOUNT); - withdrawal.amount = AMOUNT; - withdrawal.token = token; - withdrawal.tokenIndex = slot == 0 ? TOKEN0_INDEX : TOKEN1_INDEX; - withdrawal.maturity = - task == ScheduleTask.MATURE ? uint40(block.timestamp - 1) : uint40(block.timestamp + 1); - } - manager.setWithdrawalSlot(creditAccount, slot, withdrawal); - } -} diff --git a/contracts/test/unit/core/WithdrawalManagerV3Harness.sol b/contracts/test/unit/core/WithdrawalManagerV3Harness.sol deleted file mode 100644 index c57b9466..00000000 --- a/contracts/test/unit/core/WithdrawalManagerV3Harness.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Foundation, 2023. -pragma solidity ^0.8.17; - -import {ClaimAction, ScheduledWithdrawal} from "../../../interfaces/IWithdrawalManagerV3.sol"; -import {WithdrawalManagerV3} from "../../../core/WithdrawalManagerV3.sol"; - -contract WithdrawalManagerV3Harness is WithdrawalManagerV3 { - constructor(address _addressProvider, uint40 _delay) WithdrawalManagerV3(_addressProvider, _delay) {} - - function setWithdrawalSlot(address creditAccount, uint8 slot, ScheduledWithdrawal memory w) external { - _scheduled[creditAccount][slot] = w; - } - - function processScheduledWithdrawal(address creditAccount, uint8 slot, ClaimAction action, address to) - external - returns (bool scheduled, bool claimed, uint256 tokensToEnable) - { - return _processScheduledWithdrawal(_scheduled[creditAccount][slot], action, creditAccount, to); - } - - function claimScheduledWithdrawal(address creditAccount, uint8 slot, address to) external { - _claimScheduledWithdrawal(_scheduled[creditAccount][slot], creditAccount, to); - } - - function cancelScheduledWithdrawal(address creditAccount, uint8 slot) external returns (uint256 tokensToEnable) { - return _cancelScheduledWithdrawal(_scheduled[creditAccount][slot], creditAccount); - } -} diff --git a/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol b/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol index 2a70079b..fd1aa3b4 100644 --- a/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol +++ b/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol @@ -20,7 +20,6 @@ import {CreditManagerMock} from "../../mocks/credit/CreditManagerMock.sol"; import {DegenNFTMock} from "../../mocks/token/DegenNFTMock.sol"; import {AdapterMock} from "../../mocks/core/AdapterMock.sol"; import {BotListMock} from "../../mocks/core/BotListMock.sol"; -import {WithdrawalManagerMock} from "../../mocks/core/WithdrawalManagerMock.sol"; import {PriceOracleMock} from "../../mocks/oracles/PriceOracleMock.sol"; import {PriceFeedOnDemandMock} from "../../mocks/oracles/PriceFeedOnDemandMock.sol"; import {AdapterCallMock} from "../../mocks/core/AdapterCallMock.sol"; @@ -31,7 +30,6 @@ import {ENTERED} from "../../../traits/ReentrancyGuardTrait.sol"; import "../../../interfaces/ICreditFacadeV3.sol"; import { ICreditManagerV3, - ClosureAction, CollateralCalcTask, CollateralDebtData, ManageDebtAction, @@ -40,7 +38,6 @@ import { import {AllowanceAction} from "../../../interfaces/ICreditConfiguratorV3.sol"; import {IBotListV3} from "../../../interfaces/IBotListV3.sol"; -import {ClaimAction, ETH_ADDRESS, IWithdrawalManagerV3} from "../../../interfaces/IWithdrawalManagerV3.sol"; import {BitMask, UNDERLYING_TOKEN_MASK} from "../../../libraries/BitMask.sol"; import {MultiCallBuilder} from "../../lib/MultiCallBuilder.sol"; @@ -71,7 +68,6 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve CreditFacadeV3Harness creditFacade; CreditManagerMock creditManagerMock; - WithdrawalManagerMock withdrawalManagerMock; PriceOracleMock priceOracleMock; PoolMock poolMock; @@ -134,8 +130,6 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve botListMock = BotListMock(addressProvider.getAddressOrRevert(AP_BOT_LIST, 3_00)); - withdrawalManagerMock = WithdrawalManagerMock(addressProvider.getAddressOrRevert(AP_WITHDRAWAL_MANAGER, 3_00)); - priceOracleMock = PriceOracleMock(addressProvider.getAddressOrRevert(AP_PRICE_ORACLE, 3_00)); AddressProviderV3ACLMock(address(addressProvider)).addPausableAdmin(CONFIGURATOR); @@ -189,8 +183,6 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve assertEq(creditFacade.weth(), creditManagerMock.weth(), "Incorrect weth token"); - assertEq(creditFacade.withdrawalManager(), address(withdrawalManagerMock), "Incorrect withdrawalManager"); - assertEq(creditFacade.degenNFT(), address(degenNFTMock), "Incorrect degen NFT"); } @@ -205,32 +197,17 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve creditFacade.openCreditAccount({onBehalfOf: USER, calls: new MultiCall[](0), referralCode: 0}); vm.expectRevert("Pausable: paused"); - creditFacade.closeCreditAccount({ - creditAccount: DUMB_ADDRESS, - to: DUMB_ADDRESS, - skipTokenMask: 0, - convertToETH: false, - calls: new MultiCall[](0) - }); + creditFacade.closeCreditAccount({creditAccount: DUMB_ADDRESS, calls: new MultiCall[](0)}); /// @notice We'll check that it works for emergency liquidatior as exceptions in another test vm.expectRevert("Pausable: paused"); - creditFacade.liquidateCreditAccount({ - creditAccount: DUMB_ADDRESS, - to: DUMB_ADDRESS, - skipTokenMask: 0, - convertToETH: false, - calls: new MultiCall[](0) - }); + creditFacade.liquidateCreditAccount({creditAccount: DUMB_ADDRESS, to: DUMB_ADDRESS, calls: new MultiCall[](0)}); vm.expectRevert("Pausable: paused"); creditFacade.multicall({creditAccount: DUMB_ADDRESS, calls: new MultiCall[](0)}); vm.expectRevert("Pausable: paused"); creditFacade.botMulticall({creditAccount: DUMB_ADDRESS, calls: new MultiCall[](0)}); - - vm.expectRevert("Pausable: paused"); - creditFacade.claimWithdrawals({creditAccount: DUMB_ADDRESS, to: DUMB_ADDRESS}); } /// @dev U:[FA-3]: user functions revert if credit facade is expired @@ -260,22 +237,10 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve creditFacade.openCreditAccount({onBehalfOf: USER, calls: new MultiCall[](0), referralCode: 0}); vm.expectRevert("ReentrancyGuard: reentrant call"); - creditFacade.closeCreditAccount({ - creditAccount: DUMB_ADDRESS, - to: DUMB_ADDRESS, - skipTokenMask: 0, - convertToETH: false, - calls: new MultiCall[](0) - }); + creditFacade.closeCreditAccount({creditAccount: DUMB_ADDRESS, calls: new MultiCall[](0)}); vm.expectRevert("ReentrancyGuard: reentrant call"); - creditFacade.liquidateCreditAccount({ - creditAccount: DUMB_ADDRESS, - to: DUMB_ADDRESS, - skipTokenMask: 0, - convertToETH: false, - calls: new MultiCall[](0) - }); + creditFacade.liquidateCreditAccount({creditAccount: DUMB_ADDRESS, to: DUMB_ADDRESS, calls: new MultiCall[](0)}); vm.expectRevert("ReentrancyGuard: reentrant call"); creditFacade.multicall({creditAccount: DUMB_ADDRESS, calls: new MultiCall[](0)}); @@ -284,52 +249,22 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve creditFacade.botMulticall({creditAccount: DUMB_ADDRESS, calls: new MultiCall[](0)}); vm.expectRevert("ReentrancyGuard: reentrant call"); - creditFacade.claimWithdrawals({creditAccount: DUMB_ADDRESS, to: DUMB_ADDRESS}); - - vm.expectRevert("ReentrancyGuard: reentrant call"); - creditFacade.setBotPermissions({ - creditAccount: DUMB_ADDRESS, - bot: DUMB_ADDRESS, - permissions: 0, - totalFundingAllowance: 0, - weeklyFundingAllowance: 0 - }); + creditFacade.setBotPermissions({creditAccount: DUMB_ADDRESS, bot: DUMB_ADDRESS, permissions: 0}); } /// @dev U:[FA-5]: borrower related functions revert if called not by borrower function test_U_FA_05_borrower_related_functions_revert_if_called_not_by_borrower() public notExpirableCase { vm.expectRevert(CreditAccountDoesNotExistException.selector); - creditFacade.closeCreditAccount({ - creditAccount: DUMB_ADDRESS, - to: DUMB_ADDRESS, - skipTokenMask: 0, - convertToETH: false, - calls: new MultiCall[](0) - }); + creditFacade.closeCreditAccount({creditAccount: DUMB_ADDRESS, calls: new MultiCall[](0)}); vm.expectRevert(CreditAccountDoesNotExistException.selector); - creditFacade.liquidateCreditAccount({ - creditAccount: DUMB_ADDRESS, - to: DUMB_ADDRESS, - skipTokenMask: 0, - convertToETH: false, - calls: new MultiCall[](0) - }); + creditFacade.liquidateCreditAccount({creditAccount: DUMB_ADDRESS, to: DUMB_ADDRESS, calls: new MultiCall[](0)}); vm.expectRevert(CreditAccountDoesNotExistException.selector); creditFacade.multicall({creditAccount: DUMB_ADDRESS, calls: new MultiCall[](0)}); vm.expectRevert(CreditAccountDoesNotExistException.selector); - creditFacade.claimWithdrawals({creditAccount: DUMB_ADDRESS, to: DUMB_ADDRESS}); - - vm.expectRevert(CreditAccountDoesNotExistException.selector); - creditFacade.setBotPermissions({ - creditAccount: DUMB_ADDRESS, - bot: DUMB_ADDRESS, - permissions: 0, - totalFundingAllowance: 0, - weeklyFundingAllowance: 0 - }); + creditFacade.setBotPermissions({creditAccount: DUMB_ADDRESS, bot: DUMB_ADDRESS, permissions: 0}); } /// @dev U:[FA-6]: all configurator functions revert if called by non-configurator @@ -383,13 +318,7 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve creditManagerMock.setBorrower(USER); vm.prank(USER); - creditFacade.closeCreditAccount{value: 1 ether}({ - creditAccount: DUMB_ADDRESS, - to: DUMB_ADDRESS, - skipTokenMask: 0, - convertToETH: false, - calls: new MultiCall[](0) - }); + creditFacade.closeCreditAccount{value: 1 ether}({creditAccount: DUMB_ADDRESS, calls: new MultiCall[](0)}); expectBalance({t: Tokens.WETH, holder: USER, expectedBalance: 2 ether}); vm.prank(USER); @@ -466,7 +395,8 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve vm.expectCall( address(creditManagerMock), abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (expectedCreditAccount, 0, new uint256[](0), PERCENTAGE_FACTOR) + ICreditManagerV3.fullCollateralCheck, + (expectedCreditAccount, 0, new uint256[](0), PERCENTAGE_FACTOR, false) ) ); @@ -487,14 +417,14 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve } /// @dev U:[FA-11]: closeCreditAccount wokrs as expected - function test_U_FA_11_closeCreditAccount_works_as_expected(uint256 enabledTokensMask) public notExpirableCase { + function test_U_FA_11_closeCreditAccount_works_as_expected(uint256 seed) public notExpirableCase { address creditAccount = DUMB_ADDRESS; - bool hasCalls = (getHash({value: enabledTokensMask, seed: 2}) % 2) == 0; - bool hasBotPermissions = (getHash({value: enabledTokensMask, seed: 3}) % 2) == 0; - - uint256 LINK_TOKEN_MASK = 4; - uint128 debt = uint128(getHash({value: enabledTokensMask, seed: 2})) / 2; + bool hasCalls = (getHash({value: seed, seed: 2}) % 2) == 0; + bool hasBotPermissions = (getHash({value: seed, seed: 3}) % 2) == 0; + caseName = string.concat( + caseName, ", hasCalls = ", boolToStr(hasCalls), ", hasBotPermissions = ", boolToStr(hasBotPermissions) + ); address adapter = address(new AdapterMock(address(creditManagerMock), DUMB_ADDRESS)); @@ -504,77 +434,16 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve creditManagerMock.setBorrower(USER); creditManagerMock.setFlagFor(creditAccount, BOT_PERMISSIONS_SET_FLAG, hasBotPermissions); + if (!hasCalls) creditManagerMock.setRevertOnActiveAccount(true); + if (!hasBotPermissions) botListMock.setRevertOnErase(true); - CollateralDebtData memory collateralDebtData = CollateralDebtData({ - debt: debt, - cumulativeIndexNow: getHash({value: enabledTokensMask, seed: 3}), - cumulativeIndexLastUpdate: getHash({value: enabledTokensMask, seed: 4}), - cumulativeQuotaInterest: uint128(getHash({value: enabledTokensMask, seed: 5})), - accruedInterest: getHash({value: enabledTokensMask, seed: 6}), - accruedFees: getHash({value: enabledTokensMask, seed: 7}), - totalDebtUSD: 0, - totalValue: 0, - totalValueUSD: 0, - twvUSD: 0, - enabledTokensMask: enabledTokensMask, - quotedTokensMask: 0, - quotedTokens: new address[](0), - // quotedLts: new uint16[](0), - // quotas: new uint256[](0), - _poolQuotaKeeper: address(0) - }); + vm.expectCall(address(creditManagerMock), abi.encodeCall(ICreditManagerV3.enabledTokensMaskOf, (creditAccount))); - CollateralDebtData memory expectedCollateralDebtData = clone(collateralDebtData); + vm.expectCall(address(creditManagerMock), abi.encodeCall(ICreditManagerV3.closeCreditAccount, (creditAccount))); if (hasCalls) { calls = MultiCallBuilder.build( - MultiCall({target: adapter, callData: abi.encodeCall(AdapterMock.dumbCall, (LINK_TOKEN_MASK, 0))}) - ); - - expectedCollateralDebtData.enabledTokensMask |= LINK_TOKEN_MASK; - } else { - creditManagerMock.setRevertOnActiveAccount(true); - } - - creditManagerMock.setDebtAndCollateralData(collateralDebtData); - - bool convertToETH = (getHash({value: enabledTokensMask, seed: 1}) % 2) == 1; - - caseName = - string.concat(caseName, "convertToETH = ", boolToStr(convertToETH), ", hasCalls = ", boolToStr(hasCalls)); - - vm.expectCall( - address(creditManagerMock), - abi.encodeCall(ICreditManagerV3.calcDebtAndCollateral, (creditAccount, CollateralCalcTask.DEBT_ONLY)) - ); - - vm.expectCall( - address(creditManagerMock), - abi.encodeCall(ICreditManagerV3.claimWithdrawals, (creditAccount, FRIEND, ClaimAction.FORCE_CLAIM)) - ); - - uint256 skipTokenMask = getHash({value: enabledTokensMask, seed: 1}); - - vm.expectCall( - address(creditManagerMock), - abi.encodeCall( - ICreditManagerV3.closeCreditAccount, - ( - creditAccount, - ClosureAction.CLOSE_ACCOUNT, - expectedCollateralDebtData, - USER, - FRIEND, - skipTokenMask, - convertToETH - ) - ) - ); - - if (convertToETH) { - vm.expectCall( - address(withdrawalManagerMock), - abi.encodeCall(IWithdrawalManagerV3.claimImmediateWithdrawal, (ETH_ADDRESS, FRIEND)) + MultiCall({target: adapter, callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))}) ); } @@ -583,426 +452,412 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve address(botListMock), abi.encodeCall(IBotListV3.eraseAllBotPermissions, (address(creditManagerMock), creditAccount)) ); - } else { - botListMock.setRevertOnErase(true); } vm.expectEmit(true, true, true, true); - emit CloseCreditAccount(creditAccount, USER, FRIEND); + emit CloseCreditAccount(creditAccount, USER); vm.prank(USER); - creditFacade.closeCreditAccount({ - creditAccount: creditAccount, - to: FRIEND, - skipTokenMask: skipTokenMask, - convertToETH: convertToETH, - calls: calls - }); - - assertEq( - creditManagerMock.closeCollateralDebtData(), - expectedCollateralDebtData, - _testCaseErr("Incorrect collateralDebtData") - ); - } - - // - // LIQUIDATE CREDIT ACCOUNT - // - - /// @dev U:[FA-12]: liquidateCreditAccount allows emergency liquidators when paused - function test_U_FA_12_liquidateCreditAccount_allows_emergency_liquidators_when_paused() public notExpirableCase { - address creditAccount = DUMB_ADDRESS; - - creditManagerMock.setBorrower(USER); - - CollateralDebtData memory collateralDebtData; - collateralDebtData.totalDebtUSD = 101; - collateralDebtData.twvUSD = 100; - - creditManagerMock.setDebtAndCollateralData(collateralDebtData); - - vm.prank(CONFIGURATOR); - creditFacade.pause(); - - for (uint256 i = 0; i < 2; ++i) { - bool isEmergencyLiquidator = i == 0; - - vm.prank(CONFIGURATOR); - creditFacade.setEmergencyLiquidator( - LIQUIDATOR, isEmergencyLiquidator ? AllowanceAction.ALLOW : AllowanceAction.FORBID - ); - - caseName = string.concat(caseName, "isEmergencyLiquidator = ", boolToStr(isEmergencyLiquidator)); - - if (!isEmergencyLiquidator) { - vm.expectRevert("Pausable: paused"); - } - - vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount({ - creditAccount: creditAccount, - to: FRIEND, - skipTokenMask: 0, - convertToETH: false, - calls: new MultiCall[](0) - }); - } - } - - /// @dev U:[FA-13]: liquidateCreditAccount reverts if account has enough collateral - function test_U_FA_13_liquidateCreditAccount_reverts_if_account_has_enough_collateral(uint40 timestamp) - public - allExpirableCases - { - address creditAccount = DUMB_ADDRESS; - - uint40 expiredAt = uint40(getHash({value: timestamp, seed: 1}) % type(uint40).max); - - if (expirable) { - vm.prank(CONFIGURATOR); - creditFacade.setExpirationDate(expiredAt); - } - - creditManagerMock.setBorrower(USER); - - CollateralDebtData memory collateralDebtData; - collateralDebtData.totalDebtUSD = 100; - collateralDebtData.twvUSD = 100; - - creditManagerMock.setDebtAndCollateralData(collateralDebtData); - - vm.warp(timestamp); - - bool isExpiredLiquidatable = expirable && (timestamp >= expiredAt); - - if (!isExpiredLiquidatable) { - vm.expectRevert(CreditAccountNotLiquidatableException.selector); - } - - vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount({ - creditAccount: creditAccount, - to: FRIEND, - skipTokenMask: 0, - convertToETH: false, - calls: new MultiCall[](0) - }); + creditFacade.closeCreditAccount({creditAccount: creditAccount, calls: calls}); } - /// @dev U:[FA-14]: liquidateCreditAccount picks correct close action - function test_U_FA_14_liquidateCreditAccount_picks_correct_close_action(uint40 timestamp) - public - allExpirableCases - { - address creditAccount = DUMB_ADDRESS; + // // + // // LIQUIDATE CREDIT ACCOUNT + // // - uint40 expiredAt = uint40(getHash({value: timestamp, seed: 1}) % type(uint40).max); + // /// @dev U:[FA-12]: liquidateCreditAccount allows emergency liquidators when paused + // function test_U_FA_12_liquidateCreditAccount_allows_emergency_liquidators_when_paused() public notExpirableCase { + // address creditAccount = DUMB_ADDRESS; - if (expirable) { - vm.prank(CONFIGURATOR); - creditFacade.setExpirationDate(expiredAt); - } + // creditManagerMock.setBorrower(USER); - creditManagerMock.setBorrower(USER); + // CollateralDebtData memory collateralDebtData; + // collateralDebtData.totalDebtUSD = 101; + // collateralDebtData.twvUSD = 100; - vm.warp(timestamp); + // creditManagerMock.setDebtAndCollateralData(collateralDebtData); - bool isExpiredLiquidatable = expirable && (timestamp >= expiredAt); + // vm.prank(CONFIGURATOR); + // creditFacade.pause(); - bool enoughCollateral; - if (isExpiredLiquidatable) { - enoughCollateral = (getHash({value: timestamp, seed: 3}) % 2) == 0; - } + // for (uint256 i = 0; i < 2; ++i) { + // bool isEmergencyLiquidator = i == 0; - CollateralDebtData memory collateralDebtData; - collateralDebtData.totalDebtUSD = 101; - collateralDebtData.twvUSD = enoughCollateral ? 101 : 100; + // vm.prank(CONFIGURATOR); + // creditFacade.setEmergencyLiquidator( + // LIQUIDATOR, isEmergencyLiquidator ? AllowanceAction.ALLOW : AllowanceAction.FORBID + // ); - creditManagerMock.setDebtAndCollateralData(collateralDebtData); + // caseName = string.concat(caseName, "isEmergencyLiquidator = ", boolToStr(isEmergencyLiquidator)); - ClosureAction closeAction = (enoughCollateral && isExpiredLiquidatable) - ? ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT - : ClosureAction.LIQUIDATE_ACCOUNT; + // if (!isEmergencyLiquidator) { + // vm.expectRevert("Pausable: paused"); + // } - vm.expectEmit(true, true, true, true); - emit LiquidateCreditAccount(creditAccount, USER, LIQUIDATOR, FRIEND, closeAction, 0); + // vm.prank(LIQUIDATOR); + // creditFacade.liquidateCreditAccount({ + // creditAccount: creditAccount, + // to: FRIEND, + // skipTokensMask: 0, + // convertToETH: false, + // calls: new MultiCall[](0) + // }); + // } + // } - vm.expectCall( - address(creditManagerMock), - abi.encodeCall( - ICreditManagerV3.closeCreditAccount, - (creditAccount, closeAction, collateralDebtData, LIQUIDATOR, FRIEND, 0, false) - ) - ); + // /// @dev U:[FA-13]: liquidateCreditAccount reverts if account has enough collateral + // function test_U_FA_13_liquidateCreditAccount_reverts_if_account_has_enough_collateral(uint40 timestamp) + // public + // allExpirableCases + // { + // address creditAccount = DUMB_ADDRESS; - vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount({ - creditAccount: creditAccount, - to: FRIEND, - skipTokenMask: 0, - convertToETH: false, - calls: new MultiCall[](0) - }); - } + // uint40 expiredAt = uint40(getHash({value: timestamp, seed: 1}) % type(uint40).max); - /// @dev U:[FA-15]: liquidateCreditAccount claims correct withdrawal amount and enable token - function test_U_FA_15_liquidateCreditAccount_claims_correct_withdrawal_amount() public notExpirableCase { - address creditAccount = DUMB_ADDRESS; + // if (expirable) { + // vm.prank(CONFIGURATOR); + // creditFacade.setExpirationDate(expiredAt); + // } - uint256 cancelMask = 1 << 7; + // creditManagerMock.setBorrower(USER); - creditManagerMock.setBorrower(USER); + // CollateralDebtData memory collateralDebtData; + // collateralDebtData.totalDebtUSD = 100; + // collateralDebtData.twvUSD = 100; - CollateralDebtData memory collateralDebtData; - collateralDebtData.totalDebtUSD = 101; - collateralDebtData.twvUSD = 100; + // creditManagerMock.setDebtAndCollateralData(collateralDebtData); - creditManagerMock.setDebtAndCollateralData(collateralDebtData); - creditManagerMock.setClaimWithdrawals(cancelMask); + // vm.warp(timestamp); - vm.prank(CONFIGURATOR); - creditFacade.setEmergencyLiquidator(LIQUIDATOR, AllowanceAction.ALLOW); + // bool isExpiredLiquidatable = expirable && (timestamp >= expiredAt); - CollateralDebtData memory expectedCollateralDebtData = clone(collateralDebtData); - expectedCollateralDebtData.enabledTokensMask = cancelMask; + // if (!isExpiredLiquidatable) { + // vm.expectRevert(CreditAccountNotLiquidatableException.selector); + // } - for (uint256 i = 0; i < 2; ++i) { - uint256 snapshot = vm.snapshot(); - bool isEmergencyLiquidation = i == 1; + // vm.prank(LIQUIDATOR); + // creditFacade.liquidateCreditAccount({ + // creditAccount: creditAccount, + // to: FRIEND, + // skipTokensMask: 0, + // convertToETH: false, + // calls: new MultiCall[](0) + // }); + // } - if (isEmergencyLiquidation) { - vm.prank(CONFIGURATOR); - creditFacade.pause(); - } + // /// @dev U:[FA-14]: liquidateCreditAccount picks correct close action + // function test_U_FA_14_liquidateCreditAccount_picks_correct_close_action(uint40 timestamp) + // public + // allExpirableCases + // { + // address creditAccount = DUMB_ADDRESS; + + // uint40 expiredAt = uint40(getHash({value: timestamp, seed: 1}) % type(uint40).max); + + // if (expirable) { + // vm.prank(CONFIGURATOR); + // creditFacade.setExpirationDate(expiredAt); + // } + + // creditManagerMock.setBorrower(USER); + + // vm.warp(timestamp); + + // bool isExpiredLiquidatable = expirable && (timestamp >= expiredAt); + + // bool enoughCollateral; + // if (isExpiredLiquidatable) { + // enoughCollateral = (getHash({value: timestamp, seed: 3}) % 2) == 0; + // } + + // CollateralDebtData memory collateralDebtData; + // collateralDebtData.totalDebtUSD = 101; + // collateralDebtData.twvUSD = enoughCollateral ? 101 : 100; + + // creditManagerMock.setDebtAndCollateralData(collateralDebtData); + + // ClosureAction closeAction = (enoughCollateral && isExpiredLiquidatable) + // ? ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT + // : ClosureAction.LIQUIDATE_ACCOUNT; + + // vm.expectEmit(true, true, true, true); + // emit LiquidateCreditAccount(creditAccount, USER, LIQUIDATOR, FRIEND, closeAction, 0); + + // vm.expectCall( + // address(creditManagerMock), + // abi.encodeCall( + // ICreditManagerV3.closeCreditAccount, + // (creditAccount, closeAction, collateralDebtData, LIQUIDATOR, FRIEND, 0, false) + // ) + // ); + + // vm.prank(LIQUIDATOR); + // creditFacade.liquidateCreditAccount({ + // creditAccount: creditAccount, + // to: FRIEND, + // skipTokensMask: 0, + // convertToETH: false, + // calls: new MultiCall[](0) + // }); + // } + + // /// @dev U:[FA-15]: liquidateCreditAccount claims correct withdrawal amount and enable token + // function test_U_FA_15_liquidateCreditAccount_claims_correct_withdrawal_amount() public notExpirableCase { + // address creditAccount = DUMB_ADDRESS; - caseName = string.concat(caseName, "isEmergencyLiquidation = ", boolToStr(isEmergencyLiquidation)); + // uint256 cancelMask = 1 << 7; + + // creditManagerMock.setBorrower(USER); - vm.expectCall( - address(creditManagerMock), - abi.encodeCall( - ICreditManagerV3.calcDebtAndCollateral, - ( - creditAccount, - isEmergencyLiquidation - ? CollateralCalcTask.DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS - : CollateralCalcTask.DEBT_COLLATERAL_CANCEL_WITHDRAWALS - ) - ) - ); + // CollateralDebtData memory collateralDebtData; + // collateralDebtData.totalDebtUSD = 101; + // collateralDebtData.twvUSD = 100; - vm.expectCall( - address(creditManagerMock), - abi.encodeCall( - ICreditManagerV3.claimWithdrawals, - (creditAccount, USER, isEmergencyLiquidation ? ClaimAction.FORCE_CANCEL : ClaimAction.CANCEL) - ) - ); + // creditManagerMock.setDebtAndCollateralData(collateralDebtData); + // creditManagerMock.setClaimWithdrawals(cancelMask); + + // vm.prank(CONFIGURATOR); + // creditFacade.setEmergencyLiquidator(LIQUIDATOR, AllowanceAction.ALLOW); - vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount({ - creditAccount: creditAccount, - to: FRIEND, - skipTokenMask: 0, - convertToETH: false, - calls: new MultiCall[](0) - }); + // CollateralDebtData memory expectedCollateralDebtData = clone(collateralDebtData); + // expectedCollateralDebtData.enabledTokensMask = cancelMask; + + // for (uint256 i = 0; i < 2; ++i) { + // uint256 snapshot = vm.snapshot(); + // bool isEmergencyLiquidation = i == 1; + + // if (isEmergencyLiquidation) { + // vm.prank(CONFIGURATOR); + // creditFacade.pause(); + // } + + // caseName = string.concat(caseName, "isEmergencyLiquidation = ", boolToStr(isEmergencyLiquidation)); + + // vm.expectCall( + // address(creditManagerMock), + // abi.encodeCall( + // ICreditManagerV3.calcDebtAndCollateral, + // ( + // creditAccount, + // isEmergencyLiquidation + // ? CollateralCalcTask.DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS + // : CollateralCalcTask.DEBT_COLLATERAL_CANCEL_WITHDRAWALS + // ) + // ) + // ); + + // vm.expectCall( + // address(creditManagerMock), + // abi.encodeCall( + // ICreditManagerV3.claimWithdrawals, + // (creditAccount, USER, isEmergencyLiquidation ? ClaimAction.FORCE_CANCEL : ClaimAction.CANCEL) + // ) + // ); + + // vm.prank(LIQUIDATOR); + // creditFacade.liquidateCreditAccount({ + // creditAccount: creditAccount, + // to: FRIEND, + // skipTokensMask: 0, + // convertToETH: false, + // calls: new MultiCall[](0) + // }); + + // assertEq( + // creditManagerMock.liquidateCollateralDebtData(), + // expectedCollateralDebtData, + // _testCaseErr("Incorrect collateralDebtData") + // ); + + // vm.revertTo(snapshot); + // } + // } + + // /// @dev U:[FA-16]: liquidate wokrs as expected + // function test_U_FA_16_liquidate_wokrs_as_expected(uint256 enabledTokensMask) public notExpirableCase { + // address creditAccount = DUMB_ADDRESS; + + // bool hasCalls = (getHash({value: enabledTokensMask, seed: 2}) % 2) == 0; + // bool hasBotPermissions = (getHash({value: enabledTokensMask, seed: 3}) % 2) == 0; + + // uint128 debt = uint128(getHash({value: enabledTokensMask, seed: 2})) / 2; + + // uint256 cancelMask = 1 << 7; + // uint256 LINK_TOKEN_MASK = 4; + + // address adapter = address(new AdapterMock(address(creditManagerMock), DUMB_ADDRESS)); + + // creditManagerMock.setContractAllowance({adapter: adapter, targetContract: DUMB_ADDRESS}); + + // MultiCall[] memory calls; + + // creditManagerMock.setBorrower(USER); + // creditManagerMock.setFlagFor(creditAccount, BOT_PERMISSIONS_SET_FLAG, hasBotPermissions); + + // CollateralDebtData memory collateralDebtData; + // collateralDebtData.debt = debt; + // collateralDebtData.totalDebtUSD = 101; + // collateralDebtData.twvUSD = 100; + + // creditManagerMock.setDebtAndCollateralData(collateralDebtData); + // creditManagerMock.setClaimWithdrawals(cancelMask); + + // vm.prank(CONFIGURATOR); + // creditFacade.setEmergencyLiquidator(LIQUIDATOR, AllowanceAction.ALLOW); + + // CollateralDebtData memory expectedCollateralDebtData = clone(collateralDebtData); + // expectedCollateralDebtData.enabledTokensMask = cancelMask; + + // if (hasCalls) { + // calls = MultiCallBuilder.build( + // MultiCall({target: adapter, callData: abi.encodeCall(AdapterMock.dumbCall, (LINK_TOKEN_MASK, 0))}) + // ); + + // expectedCollateralDebtData.enabledTokensMask |= LINK_TOKEN_MASK; + // } else { + // creditManagerMock.setRevertOnActiveAccount(true); + // } + + // bool convertToETH = (getHash({value: enabledTokensMask, seed: 1}) % 2) == 1; + + // caseName = + // string.concat(caseName, "convertToETH = ", boolToStr(convertToETH), ", hasCalls = ", boolToStr(hasCalls)); + + // vm.expectCall( + // address(creditManagerMock), + // abi.encodeCall( + // ICreditManagerV3.calcDebtAndCollateral, + // (creditAccount, CollateralCalcTask.DEBT_COLLATERAL_CANCEL_WITHDRAWALS) + // ) + // ); + + // vm.expectCall( + // address(creditManagerMock), + // abi.encodeCall(ICreditManagerV3.claimWithdrawals, (creditAccount, USER, ClaimAction.CANCEL)) + // ); + + // uint256 skipTokensMask = getHash({value: enabledTokensMask, seed: 1}); + + // vm.expectCall( + // address(creditManagerMock), + // abi.encodeCall( + // ICreditManagerV3.closeCreditAccount, + // ( + // creditAccount, + // ClosureAction.LIQUIDATE_ACCOUNT, + // expectedCollateralDebtData, + // LIQUIDATOR, + // FRIEND, + // skipTokensMask, + // convertToETH + // ) + // ) + // ); + + // if (convertToETH) { + // vm.expectCall( + // address(withdrawalManagerMock), + // abi.encodeCall(IWithdrawalManagerV3.claimImmediateWithdrawal, (ETH_ADDRESS, FRIEND)) + // ); + // } - assertEq( - creditManagerMock.closeCollateralDebtData(), - expectedCollateralDebtData, - _testCaseErr("Incorrect collateralDebtData") - ); - - vm.revertTo(snapshot); - } - } - - /// @dev U:[FA-16]: liquidate wokrs as expected - function test_U_FA_16_liquidate_wokrs_as_expected(uint256 enabledTokensMask) public notExpirableCase { - address creditAccount = DUMB_ADDRESS; - - bool hasCalls = (getHash({value: enabledTokensMask, seed: 2}) % 2) == 0; - bool hasBotPermissions = (getHash({value: enabledTokensMask, seed: 3}) % 2) == 0; - - uint128 debt = uint128(getHash({value: enabledTokensMask, seed: 2})) / 2; - - uint256 cancelMask = 1 << 7; - uint256 LINK_TOKEN_MASK = 4; + // if (hasBotPermissions) { + // vm.expectCall( + // address(botListMock), + // abi.encodeCall(IBotListV3.eraseAllBotPermissions, (address(creditManagerMock), creditAccount)) + // ); + // } else { + // botListMock.setRevertOnErase(true); + // } + // creditManagerMock.setCloseCreditAccountReturns(1_000, 0); - address adapter = address(new AdapterMock(address(creditManagerMock), DUMB_ADDRESS)); + // vm.expectEmit(true, true, true, true); + // emit LiquidateCreditAccount(creditAccount, USER, LIQUIDATOR, FRIEND, ClosureAction.LIQUIDATE_ACCOUNT, 1_000); - creditManagerMock.setContractAllowance({adapter: adapter, targetContract: DUMB_ADDRESS}); + // vm.prank(LIQUIDATOR); + // creditFacade.liquidateCreditAccount({ + // creditAccount: creditAccount, + // to: FRIEND, + // skipTokensMask: skipTokensMask, + // convertToETH: convertToETH, + // calls: calls + // }); + + // assertEq( + // creditManagerMock.liquidateCollateralDebtData(), + // expectedCollateralDebtData, + // _testCaseErr("Incorrect collateralDebtData") + // ); + // } + + // /// @dev U:[FA-17]: liquidate correctly computes cumulative loss and pause contract if needed + // function test_U_FA_17_liquidate_correctly_computes_cumulative_loss_and_pause_contract_if_needed(uint128 maxLoss) + // public + // notExpirableCase + // { + // vm.assume(maxLoss > 0 && maxLoss < type(uint120).max); + + // address creditAccount = DUMB_ADDRESS; + + // MultiCall[] memory calls; + + // vm.prank(CONFIGURATOR); + // creditFacade.setCumulativeLossParams(maxLoss, true); + + // vm.prank(CONFIGURATOR); + // creditFacade.setDebtLimits(1, 100, 10); + + // assertEq(creditFacade.maxDebtPerBlockMultiplier(), 10, "SETUP: incorrect maxDebtPerBlockMultiplier"); + + // uint256 step = maxLoss / ((getHash(maxLoss, 3) % 5) + 1) + 1; + + // uint256 expectedCumulativeLoss; + + // creditManagerMock.setBorrower(USER); + + // CollateralDebtData memory collateralDebtData; + // collateralDebtData.totalDebtUSD = 101; + // collateralDebtData.twvUSD = 100; + + // creditManagerMock.setDebtAndCollateralData(collateralDebtData); + // creditManagerMock.setClaimWithdrawals(0); + + // creditManagerMock.setCloseCreditAccountReturns(1000, step); + + // do { + // vm.expectCall( + // address(creditManagerMock), + // abi.encodeCall( + // ICreditManagerV3.closeCreditAccount, + // (creditAccount, ClosureAction.LIQUIDATE_ACCOUNT, collateralDebtData, LIQUIDATOR, FRIEND, 0, false) + // ) + // ); + + // vm.expectEmit(true, true, true, true); + // emit LiquidateCreditAccount(creditAccount, USER, LIQUIDATOR, FRIEND, ClosureAction.LIQUIDATE_ACCOUNT, 1_000); + + // vm.prank(LIQUIDATOR); + // creditFacade.liquidateCreditAccount({ + // creditAccount: creditAccount, + // to: FRIEND, + // skipTokensMask: 0, + // convertToETH: false, + // calls: calls + // }); + + // assertEq(creditFacade.maxDebtPerBlockMultiplier(), 0, "maxDebtPerBlockMultiplier wasnt set to zero"); + + // (uint128 currentCumulativeLoss,) = creditFacade.lossParams(); - MultiCall[] memory calls; + // expectedCumulativeLoss += step; - creditManagerMock.setBorrower(USER); - creditManagerMock.setFlagFor(creditAccount, BOT_PERMISSIONS_SET_FLAG, hasBotPermissions); + // assertEq(currentCumulativeLoss, expectedCumulativeLoss, "Incorrect currentCumulativeLoss"); - CollateralDebtData memory collateralDebtData; - collateralDebtData.debt = debt; - collateralDebtData.totalDebtUSD = 101; - collateralDebtData.twvUSD = 100; + // bool shoudBePaused = expectedCumulativeLoss > maxLoss; - creditManagerMock.setDebtAndCollateralData(collateralDebtData); - creditManagerMock.setClaimWithdrawals(cancelMask); - - vm.prank(CONFIGURATOR); - creditFacade.setEmergencyLiquidator(LIQUIDATOR, AllowanceAction.ALLOW); - - CollateralDebtData memory expectedCollateralDebtData = clone(collateralDebtData); - expectedCollateralDebtData.enabledTokensMask = cancelMask; - - if (hasCalls) { - calls = MultiCallBuilder.build( - MultiCall({target: adapter, callData: abi.encodeCall(AdapterMock.dumbCall, (LINK_TOKEN_MASK, 0))}) - ); - - expectedCollateralDebtData.enabledTokensMask |= LINK_TOKEN_MASK; - } else { - creditManagerMock.setRevertOnActiveAccount(true); - } - - bool convertToETH = (getHash({value: enabledTokensMask, seed: 1}) % 2) == 1; - - caseName = - string.concat(caseName, "convertToETH = ", boolToStr(convertToETH), ", hasCalls = ", boolToStr(hasCalls)); - - vm.expectCall( - address(creditManagerMock), - abi.encodeCall( - ICreditManagerV3.calcDebtAndCollateral, - (creditAccount, CollateralCalcTask.DEBT_COLLATERAL_CANCEL_WITHDRAWALS) - ) - ); - - vm.expectCall( - address(creditManagerMock), - abi.encodeCall(ICreditManagerV3.claimWithdrawals, (creditAccount, USER, ClaimAction.CANCEL)) - ); - - uint256 skipTokenMask = getHash({value: enabledTokensMask, seed: 1}); - - vm.expectCall( - address(creditManagerMock), - abi.encodeCall( - ICreditManagerV3.closeCreditAccount, - ( - creditAccount, - ClosureAction.LIQUIDATE_ACCOUNT, - expectedCollateralDebtData, - LIQUIDATOR, - FRIEND, - skipTokenMask, - convertToETH - ) - ) - ); - - if (convertToETH) { - vm.expectCall( - address(withdrawalManagerMock), - abi.encodeCall(IWithdrawalManagerV3.claimImmediateWithdrawal, (ETH_ADDRESS, FRIEND)) - ); - } - - if (hasBotPermissions) { - vm.expectCall( - address(botListMock), - abi.encodeCall(IBotListV3.eraseAllBotPermissions, (address(creditManagerMock), creditAccount)) - ); - } else { - botListMock.setRevertOnErase(true); - } - creditManagerMock.setCloseCreditAccountReturns(1_000, 0); - - vm.expectEmit(true, true, true, true); - emit LiquidateCreditAccount(creditAccount, USER, LIQUIDATOR, FRIEND, ClosureAction.LIQUIDATE_ACCOUNT, 1_000); - - vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount({ - creditAccount: creditAccount, - to: FRIEND, - skipTokenMask: skipTokenMask, - convertToETH: convertToETH, - calls: calls - }); - - assertEq( - creditManagerMock.closeCollateralDebtData(), - expectedCollateralDebtData, - _testCaseErr("Incorrect collateralDebtData") - ); - } - - /// @dev U:[FA-17]: liquidate correctly computes cumulative loss and pause contract if needed - function test_U_FA_17_liquidate_correctly_computes_cumulative_loss_and_pause_contract_if_needed(uint128 maxLoss) - public - notExpirableCase - { - vm.assume(maxLoss > 0 && maxLoss < type(uint120).max); - - address creditAccount = DUMB_ADDRESS; - - MultiCall[] memory calls; - - vm.prank(CONFIGURATOR); - creditFacade.setCumulativeLossParams(maxLoss, true); - - vm.prank(CONFIGURATOR); - creditFacade.setDebtLimits(1, 100, 10); - - assertEq(creditFacade.maxDebtPerBlockMultiplier(), 10, "SETUP: incorrect maxDebtPerBlockMultiplier"); - - uint256 step = maxLoss / ((getHash(maxLoss, 3) % 5) + 1) + 1; - - uint256 expectedCumulativeLoss; - - creditManagerMock.setBorrower(USER); - - CollateralDebtData memory collateralDebtData; - collateralDebtData.totalDebtUSD = 101; - collateralDebtData.twvUSD = 100; - - creditManagerMock.setDebtAndCollateralData(collateralDebtData); - creditManagerMock.setClaimWithdrawals(0); - - creditManagerMock.setCloseCreditAccountReturns(1000, step); - - do { - vm.expectCall( - address(creditManagerMock), - abi.encodeCall( - ICreditManagerV3.closeCreditAccount, - (creditAccount, ClosureAction.LIQUIDATE_ACCOUNT, collateralDebtData, LIQUIDATOR, FRIEND, 0, false) - ) - ); - - vm.expectEmit(true, true, true, true); - emit LiquidateCreditAccount(creditAccount, USER, LIQUIDATOR, FRIEND, ClosureAction.LIQUIDATE_ACCOUNT, 1_000); - - vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount({ - creditAccount: creditAccount, - to: FRIEND, - skipTokenMask: 0, - convertToETH: false, - calls: calls - }); - - assertEq(creditFacade.maxDebtPerBlockMultiplier(), 0, "maxDebtPerBlockMultiplier wasnt set to zero"); - - (uint128 currentCumulativeLoss,) = creditFacade.lossParams(); - - expectedCumulativeLoss += step; - - assertEq(currentCumulativeLoss, expectedCumulativeLoss, "Incorrect currentCumulativeLoss"); - - bool shoudBePaused = expectedCumulativeLoss > maxLoss; - - assertEq(creditFacade.paused(), shoudBePaused, "Paused wasn't set"); - } while (expectedCumulativeLoss < maxLoss); - } + // assertEq(creditFacade.paused(), shoudBePaused, "Paused wasn't set"); + // } while (expectedCumulativeLoss < maxLoss); + // } // // @@ -1035,7 +890,7 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve address(creditManagerMock), abi.encodeCall( ICreditManagerV3.fullCollateralCheck, - (creditAccount, enabledTokensMaskAfter, new uint256[](0), PERCENTAGE_FACTOR) + (creditAccount, enabledTokensMaskAfter, new uint256[](0), PERCENTAGE_FACTOR, false) ) ); @@ -1083,53 +938,6 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve creditFacade.botMulticall(creditAccount, calls); } - /// @dev U:[FA-20]: only botMulticall doesn't revert for pay bot call - function test_U_FA_20_only_botMulticall_doesnt_revert_for_pay_bot_call() public notExpirableCase { - address creditAccount = DUMB_ADDRESS; - - creditManagerMock.setFlagFor(creditAccount, BOT_PERMISSIONS_SET_FLAG, true); - - vm.prank(CONFIGURATOR); - creditFacade.setDebtLimits(1, 100, 1); - - creditManagerMock.setBorrower(USER); - botListMock.setBotStatusReturns(ALL_PERMISSIONS, false, false); - - MultiCall[] memory calls = MultiCallBuilder.build( - MultiCall({target: address(creditFacade), callData: abi.encodeCall(ICreditFacadeV3Multicall.payBot, (1))}) - ); - - vm.expectRevert(abi.encodeWithSelector(NoPermissionException.selector, PAY_BOT_CAN_BE_CALLED)); - creditFacade.openCreditAccount({onBehalfOf: USER, calls: calls, referralCode: 0}); - - vm.prank(USER); - vm.expectRevert(abi.encodeWithSelector(NoPermissionException.selector, PAY_BOT_CAN_BE_CALLED)); - creditFacade.multicall(creditAccount, calls); - - /// Case: it works for bot multicall - creditFacade.botMulticall(creditAccount, calls); - } - - /// @dev U:[FA-20A]: botMulticall reverts for payBot calls for bots with special permissions - function test_U_FA_20A_payBot_reverts_for_special_bots() public notExpirableCase { - address creditAccount = DUMB_ADDRESS; - - creditManagerMock.setFlagFor(creditAccount, BOT_PERMISSIONS_SET_FLAG, true); - - vm.prank(CONFIGURATOR); - creditFacade.setDebtLimits(1, 100, 1); - - creditManagerMock.setBorrower(USER); - botListMock.setBotStatusReturns(ALL_PERMISSIONS, false, true); - - MultiCall[] memory calls = MultiCallBuilder.build( - MultiCall({target: address(creditFacade), callData: abi.encodeCall(ICreditFacadeV3Multicall.payBot, (1))}) - ); - - vm.expectRevert(abi.encodeWithSelector(NoPermissionException.selector, PAY_BOT_CAN_BE_CALLED)); - creditFacade.botMulticall(creditAccount, calls); - } - struct MultiCallPermissionTestCase { bytes callData; uint256 permissionRquired; @@ -1155,7 +963,7 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve creditManagerMock.setBorrower(USER); - MultiCallPermissionTestCase[10] memory cases = [ + MultiCallPermissionTestCase[9] memory cases = [ MultiCallPermissionTestCase({ callData: abi.encodeCall(ICreditFacadeV3Multicall.enableToken, (token)), permissionRquired: ENABLE_TOKEN_PERMISSION @@ -1187,16 +995,12 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve permissionRquired: UPDATE_QUOTA_PERMISSION }), MultiCallPermissionTestCase({ - callData: abi.encodeCall(ICreditFacadeV3Multicall.scheduleWithdrawal, (token, 0)), - permissionRquired: WITHDRAW_PERMISSION + callData: abi.encodeCall(ICreditFacadeV3Multicall.withdrawCollateral, (token, 0, USER)), + permissionRquired: WITHDRAW_COLLATERAL_PERMISSION }), MultiCallPermissionTestCase({ callData: abi.encodeCall(ICreditFacadeV3Multicall.revokeAdapterAllowances, (new RevocationPair[](0))), permissionRquired: REVOKE_ALLOWANCES_PERMISSION - }), - MultiCallPermissionTestCase({ - callData: abi.encodeCall(ICreditFacadeV3Multicall.payBot, (0)), - permissionRquired: PAY_BOT_CAN_BE_CALLED }) ]; @@ -1343,11 +1147,11 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve MultiCall[] memory calls = MultiCallBuilder.build( MultiCall({ target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeV3Multicall.onDemandPriceUpdate, (token, cd)) + callData: abi.encodeCall(ICreditFacadeV3Multicall.onDemandPriceUpdate, (token, false, cd)) }) ); - vm.expectCall(address(priceOracleMock), abi.encodeCall(IPriceOracleBase.priceFeeds, (token))); + // vm.expectCall(address(priceOracleMock), abi.encodeCall(IPriceOracleBase.priceFeeds, (token))); vm.expectCall(address(priceFeedOnDemandMock), abi.encodeCall(PriceFeedOnDemandMock.updatePrice, (cd))); creditFacade.applyPriceOnDemandInt({calls: calls}); @@ -1355,7 +1159,7 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve calls = MultiCallBuilder.build( MultiCall({ target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeV3Multicall.onDemandPriceUpdate, (DUMB_ADDRESS, cd)) + callData: abi.encodeCall(ICreditFacadeV3Multicall.onDemandPriceUpdate, (DUMB_ADDRESS, false, cd)) }) ); @@ -1566,8 +1370,11 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve }); } - /// @dev U:[FA-30]: multicall increase debt / schedule withdrawal if forbid tokens on account - function test_U_FA_30_multicall_increase_debt_if_forbid_tokens_on_account() public notExpirableCase { + /// @dev U:[FA-30]: multicall increaseDebt / withdrawCollateral set revertOnForbiddenTokens flag + function test_U_FA_30_multicall_increaseDebt_and_withdrawCollateral_set_revertOnForbiddenTokens() + public + notExpirableCase + { address creditAccount = DUMB_ADDRESS; address link = tokenTestSuite.addressOf(Tokens.LINK); @@ -1584,16 +1391,7 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve creditManagerMock.setManageDebt({newDebt: 50, tokensToEnable: UNDERLYING_TOKEN_MASK, tokensToDisable: 0}); - vm.expectRevert(ForbiddenTokensException.selector); - creditFacade.multicallInt({ - creditAccount: creditAccount, - calls: MultiCallBuilder.build(), - enabledTokensMask: linkMask, - flags: REVERT_ON_FORBIDDEN_TOKENS_AFTER_CALLS - }); - - vm.expectRevert(ForbiddenTokensException.selector); - creditFacade.multicallInt({ + FullCheckParams memory params = creditFacade.multicallInt({ creditAccount: creditAccount, calls: MultiCallBuilder.build( MultiCall({ @@ -1604,19 +1402,20 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve enabledTokensMask: linkMask, flags: INCREASE_DEBT_PERMISSION }); + assertTrue(params.revertOnForbiddenTokens, "revertOnForbiddenTokens is false after increaseDebt"); - vm.expectRevert(ForbiddenTokensException.selector); - creditFacade.multicallInt({ + params = creditFacade.multicallInt({ creditAccount: creditAccount, calls: MultiCallBuilder.build( MultiCall({ target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeV3Multicall.scheduleWithdrawal, (link, 1000)) + callData: abi.encodeCall(ICreditFacadeV3Multicall.withdrawCollateral, (link, 1000, USER)) }) ), enabledTokensMask: linkMask, - flags: WITHDRAW_PERMISSION + flags: WITHDRAW_COLLATERAL_PERMISSION }); + assertTrue(params.revertOnForbiddenTokens, "revertOnForbiddenTokens is false after withdrawCollateral"); } /// @@ -1862,19 +1661,19 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve }); } - /// @dev U:[FA-35]: multicall `scheduleWithdrawal` works properly - function test_U_FA_35_multicall_scheduleWithdrawal_works_properly() public notExpirableCase { + /// @dev U:[FA-35]: multicall `withdrawCollateral` works properly + function test_U_FA_35_multicall_withdrawCollateral_works_properly() public notExpirableCase { address creditAccount = DUMB_ADDRESS; address link = tokenTestSuite.addressOf(Tokens.LINK); uint256 maskToDisable = 1 << 7; uint256 amount = 100; - creditManagerMock.setScheduleWithdrawal({tokensToDisable: maskToDisable}); + creditManagerMock.setWithdrawCollateral({tokensToDisable: maskToDisable}); vm.expectCall( address(creditManagerMock), - abi.encodeCall(ICreditManagerV3.scheduleWithdrawal, (creditAccount, link, amount)) + abi.encodeCall(ICreditManagerV3.withdrawCollateral, (creditAccount, link, amount, USER)) ); FullCheckParams memory fullCheckParams = creditFacade.multicallInt({ @@ -1882,11 +1681,11 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve calls: MultiCallBuilder.build( MultiCall({ target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeV3Multicall.scheduleWithdrawal, (link, amount)) + callData: abi.encodeCall(ICreditFacadeV3Multicall.withdrawCollateral, (link, amount, USER)) }) ), enabledTokensMask: maskToDisable | UNDERLYING_TOKEN_MASK, - flags: WITHDRAW_PERMISSION + flags: WITHDRAW_COLLATERAL_PERMISSION }); assertEq( @@ -1919,52 +1718,6 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve }); } - /// @dev U:[FA-37]: multicall payBot works properly - function test_U_FA_37_multicall_payBot_works_properly() public notExpirableCase { - address creditAccount = DUMB_ADDRESS; - - creditManagerMock.setBorrower(USER); - - uint72 paymentAmount = 100_000; - - address bot = makeAddr("BOT"); - vm.expectCall( - address(botListMock), - abi.encodeCall(IBotListV3.payBot, (USER, address(creditManagerMock), creditAccount, bot, paymentAmount)) - ); - - vm.prank(bot); - creditFacade.multicallInt({ - creditAccount: creditAccount, - calls: MultiCallBuilder.build( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeV3Multicall.payBot, (paymentAmount)) - }) - ), - enabledTokensMask: 0, - flags: PAY_BOT_CAN_BE_CALLED - }); - - vm.expectRevert(abi.encodeWithSelector(NoPermissionException.selector, PAY_BOT_CAN_BE_CALLED)); - vm.prank(bot); - creditFacade.multicallInt({ - creditAccount: creditAccount, - calls: MultiCallBuilder.build( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeV3Multicall.payBot, (paymentAmount)) - }), - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeV3Multicall.payBot, (paymentAmount)) - }) - ), - enabledTokensMask: 0, - flags: PAY_BOT_CAN_BE_CALLED - }); - } - struct ExternalCallTestCase { string name; uint256 quotedTokensMask; @@ -2060,22 +1813,6 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve creditFacade.revertIfNoPermission(mask & ~(permission), permission); } - /// @dev U:[FA-40]: claimWithdrawals calls works properly - function test_U_FA_40_claimWithdrawals_calls_properly() public notExpirableCase { - address creditAccount = DUMB_ADDRESS; - address to = makeAddr("TO"); - - creditManagerMock.setBorrower(USER); - - vm.expectCall( - address(creditManagerMock), - abi.encodeCall(ICreditManagerV3.claimWithdrawals, (creditAccount, to, ClaimAction.CLAIM)) - ); - - vm.prank(USER); - creditFacade.claimWithdrawals(creditAccount, to); - } - /// @dev U:[FA-41]: setBotPermissions calls works properly function test_U_FA_41_setBotPermissions_calls_properly() public notExpirableCase { address creditAccount = DUMB_ADDRESS; @@ -2083,6 +1820,11 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve creditManagerMock.setBorrower(USER); + /// It reverts if passed unexpected permissions + vm.expectRevert(UnexpectedPermissionsException.selector); + vm.prank(USER); + creditFacade.setBotPermissions({creditAccount: creditAccount, bot: bot, permissions: type(uint192).max}); + creditManagerMock.setFlagFor({creditAccount: creditAccount, flag: BOT_PERMISSIONS_SET_FLAG, value: false}); botListMock.setBotPermissionsReturn(1); @@ -2095,35 +1837,23 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve ); vm.expectCall( address(botListMock), - abi.encodeCall(IBotListV3.setBotPermissions, (address(creditManagerMock), creditAccount, bot, 1, 2, 3)) + abi.encodeCall(IBotListV3.setBotPermissions, (bot, address(creditManagerMock), creditAccount, 1)) ); vm.prank(USER); - creditFacade.setBotPermissions({ - creditAccount: creditAccount, - bot: bot, - permissions: 1, - totalFundingAllowance: 2, - weeklyFundingAllowance: 3 - }); + creditFacade.setBotPermissions({creditAccount: creditAccount, bot: bot, permissions: 1}); /// It reverts if too many bots approved botListMock.setBotPermissionsReturn(creditFacade.maxApprovedBots() + 1); vm.expectRevert(TooManyApprovedBotsException.selector); vm.prank(USER); - creditFacade.setBotPermissions({ - creditAccount: creditAccount, - bot: bot, - permissions: 1, - totalFundingAllowance: 2, - weeklyFundingAllowance: 3 - }); + creditFacade.setBotPermissions({creditAccount: creditAccount, bot: bot, permissions: 1}); /// It removes flag if no bots left botListMock.setBotPermissionsReturn(0); vm.expectCall( address(botListMock), - abi.encodeCall(IBotListV3.setBotPermissions, (address(creditManagerMock), creditAccount, bot, 1, 2, 3)) + abi.encodeCall(IBotListV3.setBotPermissions, (bot, address(creditManagerMock), creditAccount, 1)) ); vm.expectCall( @@ -2131,30 +1861,7 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve abi.encodeCall(ICreditManagerV3.setFlagFor, (creditAccount, BOT_PERMISSIONS_SET_FLAG, false)) ); vm.prank(USER); - creditFacade.setBotPermissions({ - creditAccount: creditAccount, - bot: bot, - permissions: 1, - totalFundingAllowance: 2, - weeklyFundingAllowance: 3 - }); - } - - /// @dev U:[FA-42]: eraseAllBotPermissions works properly - function test_U_FA_42_eraseAllBotPermissions_works_properly() public notExpirableCase { - address creditAccount = DUMB_ADDRESS; - - botListMock.setRevertOnErase(true); - creditManagerMock.setFlagFor({creditAccount: creditAccount, flag: BOT_PERMISSIONS_SET_FLAG, value: false}); - creditFacade.eraseAllBotPermissions(creditAccount); - - botListMock.setRevertOnErase(false); - creditManagerMock.setFlagFor({creditAccount: creditAccount, flag: BOT_PERMISSIONS_SET_FLAG, value: true}); - vm.expectCall( - address(botListMock), - abi.encodeCall(IBotListV3.eraseAllBotPermissions, (address(creditManagerMock), creditAccount)) - ); - creditFacade.eraseAllBotPermissions(creditAccount); + creditFacade.setBotPermissions({creditAccount: creditAccount, bot: bot, permissions: 1}); } /// @dev U:[FA-43]: revertIfOutOfBorrowingLimit works properly diff --git a/contracts/test/unit/credit/CreditFacadeV3Harness.sol b/contracts/test/unit/credit/CreditFacadeV3Harness.sol index 76650f7a..1fa67db0 100644 --- a/contracts/test/unit/credit/CreditFacadeV3Harness.sol +++ b/contracts/test/unit/credit/CreditFacadeV3Harness.sol @@ -32,10 +32,6 @@ contract CreditFacadeV3Harness is CreditFacadeV3 { _revertIfNoPermission(flags, permission); } - function eraseAllBotPermissions(address creditAccount) external { - _eraseAllBotPermissions(creditAccount); - } - function revertIfOutOfBorrowingLimit(uint256 amount) external { _revertIfOutOfBorrowingLimit(amount); } diff --git a/contracts/test/unit/credit/CreditManagerV3.unit.t.sol b/contracts/test/unit/credit/CreditManagerV3.unit.t.sol index a0694483..cfcc2f9a 100644 --- a/contracts/test/unit/credit/CreditManagerV3.unit.t.sol +++ b/contracts/test/unit/credit/CreditManagerV3.unit.t.sol @@ -25,21 +25,18 @@ import {ENTERED} from "../../../traits/ReentrancyGuardTrait.sol"; import {ICreditAccountBase} from "../../../interfaces/ICreditAccountV3.sol"; import { ICreditManagerV3, - ClosureAction, CollateralTokenData, ManageDebtAction, CreditAccountInfo, RevocationPair, CollateralDebtData, CollateralCalcTask, - ICreditManagerV3Events, - WITHDRAWAL_FLAG + ICreditManagerV3Events } from "../../../interfaces/ICreditManagerV3.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {ClaimAction, IWithdrawalManagerV3} from "../../../interfaces/IWithdrawalManagerV3.sol"; import {IPoolQuotaKeeperV3} from "../../../interfaces/IPoolQuotaKeeperV3.sol"; // EXCEPTIONS @@ -51,7 +48,6 @@ import {PoolQuotaKeeperMock} from "../../mocks/pool/PoolQuotaKeeperMock.sol"; import {ERC20FeeMock} from "../../mocks/token/ERC20FeeMock.sol"; import {ERC20Mock} from "../../mocks/token/ERC20Mock.sol"; import {CreditAccountMock, CreditAccountMockEvents} from "../../mocks/credit/CreditAccountMock.sol"; -import {WithdrawalManagerMock} from "../../mocks/core/WithdrawalManagerMock.sol"; // SUITES import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; import {Tokens} from "@gearbox-protocol/sdk-gov/contracts/Tokens.sol"; @@ -65,6 +61,8 @@ import "../../lib/constants.sol"; import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; import {TestHelper, Vars, VarU256} from "../../lib/helper.sol"; +import "forge-std/console.sol"; + uint16 constant LT_UNDERLYING = uint16(PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM - DEFAULT_FEE_LIQUIDATION); contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceHelper, CreditAccountMockEvents { @@ -85,7 +83,6 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH PoolQuotaKeeperMock poolQuotaKeeperMock; PriceOracleMock priceOracleMock; - WithdrawalManagerMock withdrawalManagerMock; address underlying; @@ -125,7 +122,6 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH addressProvider.setAddress(AP_WETH_TOKEN, tokenTestSuite.addressOf(Tokens.WETH), false); accountFactory = AccountFactoryMock(addressProvider.getAddressOrRevert(AP_ACCOUNT_FACTORY, NO_VERSION_CONTROL)); - withdrawalManagerMock = WithdrawalManagerMock(addressProvider.getAddressOrRevert(AP_WITHDRAWAL_MANAGER, 3_00)); priceOracleMock = PriceOracleMock(addressProvider.getAddressOrRevert(AP_PRICE_ORACLE, 3_00)); @@ -263,13 +259,8 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH if (task == CollateralCalcTask.DEBT_ONLY) return "DEBT_ONLY"; - if (task == CollateralCalcTask.DEBT_COLLATERAL_WITHOUT_WITHDRAWALS) { - return "DEBT_COLLATERAL_WITHOUT_WITHDRAWALS"; - } - if (task == CollateralCalcTask.DEBT_COLLATERAL_CANCEL_WITHDRAWALS) return "DEBT_COLLATERAL_CANCEL_WITHDRAWALS"; - - if (task == CollateralCalcTask.DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS) { - return "DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS"; + if (task == CollateralCalcTask.DEBT_COLLATERAL) { + return "DEBT_COLLATERAL"; } if (task == CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY) return "FULL_COLLATERAL_CHECK_LAZY"; @@ -307,12 +298,6 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH _testCaseErr("Incorrect WETH token") ); - assertEq( - creditManager.withdrawalManager(), - addressProvider.getAddressOrRevert(AP_WITHDRAWAL_MANAGER, 3_00), - _testCaseErr("Incorrect withdrawalManager") - ); - assertEq( address(creditManager.priceOracle()), addressProvider.getAddressOrRevert(AP_PRICE_ORACLE, 3_00), @@ -348,17 +333,16 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH vm.expectRevert(CallerNotCreditFacadeException.selector); creditManager.openCreditAccount(address(this)); - CollateralDebtData memory collateralDebtData; + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.closeCreditAccount({creditAccount: DUMB_ADDRESS}); + CollateralDebtData memory collateralDebtData; vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.closeCreditAccount({ + creditManager.liquidateCreditAccount({ creditAccount: DUMB_ADDRESS, - closureAction: ClosureAction.LIQUIDATE_ACCOUNT, collateralDebtData: collateralDebtData, - payer: DUMB_ADDRESS, to: DUMB_ADDRESS, - skipTokensMask: 0, - convertToETH: false + isExpired: false }); vm.expectRevert(CallerNotCreditFacadeException.selector); @@ -368,16 +352,13 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH creditManager.addCollateral(DUMB_ADDRESS, DUMB_ADDRESS, DUMB_ADDRESS, 100); vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.fullCollateralCheck(DUMB_ADDRESS, 0, new uint256[](0), 1); + creditManager.fullCollateralCheck(DUMB_ADDRESS, 0, new uint256[](0), 1, false); vm.expectRevert(CallerNotCreditFacadeException.selector); creditManager.updateQuota(DUMB_ADDRESS, DUMB_ADDRESS, 0, 0, 0); vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.scheduleWithdrawal(DUMB_ADDRESS, DUMB_ADDRESS, 0); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.claimWithdrawals(DUMB_ADDRESS, DUMB_ADDRESS, ClaimAction.CLAIM); + creditManager.withdrawCollateral(DUMB_ADDRESS, DUMB_ADDRESS, 0, USER); vm.expectRevert(CallerNotCreditFacadeException.selector); creditManager.revokeAdapterAllowances(DUMB_ADDRESS, new RevocationPair[](0)); @@ -446,17 +427,16 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH vm.expectRevert("ReentrancyGuard: reentrant call"); creditManager.openCreditAccount(address(this)); - CollateralDebtData memory collateralDebtData; + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.closeCreditAccount({creditAccount: DUMB_ADDRESS}); + CollateralDebtData memory collateralDebtData; vm.expectRevert("ReentrancyGuard: reentrant call"); - creditManager.closeCreditAccount({ + creditManager.liquidateCreditAccount({ creditAccount: DUMB_ADDRESS, - closureAction: ClosureAction.LIQUIDATE_ACCOUNT, collateralDebtData: collateralDebtData, - payer: DUMB_ADDRESS, to: DUMB_ADDRESS, - skipTokensMask: 0, - convertToETH: false + isExpired: false }); vm.expectRevert("ReentrancyGuard: reentrant call"); @@ -466,16 +446,13 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH creditManager.addCollateral(DUMB_ADDRESS, DUMB_ADDRESS, DUMB_ADDRESS, 100); vm.expectRevert("ReentrancyGuard: reentrant call"); - creditManager.fullCollateralCheck(DUMB_ADDRESS, 0, new uint256[](0), 1); + creditManager.fullCollateralCheck(DUMB_ADDRESS, 0, new uint256[](0), 1, false); vm.expectRevert("ReentrancyGuard: reentrant call"); creditManager.updateQuota(DUMB_ADDRESS, DUMB_ADDRESS, 0, 0, 0); vm.expectRevert("ReentrancyGuard: reentrant call"); - creditManager.scheduleWithdrawal(DUMB_ADDRESS, DUMB_ADDRESS, 0); - - vm.expectRevert("ReentrancyGuard: reentrant call"); - creditManager.claimWithdrawals(DUMB_ADDRESS, DUMB_ADDRESS, ClaimAction.CLAIM); + creditManager.withdrawCollateral(DUMB_ADDRESS, DUMB_ADDRESS, 0, USER); vm.expectRevert("ReentrancyGuard: reentrant call"); creditManager.revokeAdapterAllowances(DUMB_ADDRESS, new RevocationPair[](0)); @@ -501,106 +478,96 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH /// @dev U:[CM-6]: open credit account works as expected function test_U_CM_06_open_credit_account_works_as_expected() public creditManagerTest { - uint256 cumulativeIndexNow = RAY * 5; - - vm.roll(892323); - creditManager.setCollateralTokensCount(255); - - poolMock.setCumulativeIndexNow(cumulativeIndexNow); - - tokenTestSuite.mint(Tokens.DAI, address(poolMock), DAI_ACCOUNT_AMOUNT); - assertEq(creditManager.creditAccounts().length, 0, _testCaseErr("SETUP: incorrect creditAccounts() length")); - uint128 cumulativeQuotaInterestBefore = 123412321; - uint256 enabledTokensMaskBefore = 231423; + address expectedAccount = accountFactory.usedAccount(); creditManager.setCreditAccountInfoMap({ - creditAccount: accountFactory.usedAccount(), - debt: 12039120, - cumulativeIndexLastUpdate: 23e3, - cumulativeQuotaInterest: cumulativeQuotaInterestBefore, - quotaFees: 0, - enabledTokensMask: enabledTokensMaskBefore, + creditAccount: expectedAccount, + debt: 0, + cumulativeIndexLastUpdate: 0, + cumulativeQuotaInterest: 12121, + quotaFees: 23232, + enabledTokensMask: 0, flags: 34343, borrower: address(0) }); + creditManager.setLastDebtUpdate(expectedAccount, 123); - // todo: check why expectCall doesn't work - // vm.expectCall(address(accountFactory), abi.encodeCall(IAccountFactory.takeCreditAccount, (0, 0))); + vm.expectCall(address(accountFactory), abi.encodeCall(accountFactory.takeCreditAccount, (0, 0))); address creditAccount = creditManager.openCreditAccount(USER); - assertEq( - address(creditAccount), accountFactory.usedAccount(), _testCaseErr("Incorrect credit account returned") - ); - ( - uint256 debt, - uint256 cumulativeIndexLastUpdate, - uint128 cumulativeQuotaInterest, - , - uint256 enabledTokensMask, - uint16 flags, - uint64 lastDebtUpdate, - address borrower - ) = creditManager.creditAccountInfo(creditAccount); - - assertEq(debt, 0, _testCaseErr("Incorrect debt")); - assertEq(cumulativeIndexLastUpdate, cumulativeIndexNow, _testCaseErr("Incorrect cumulativeIndexLastUpdate")); - assertEq(cumulativeQuotaInterest, 1, _testCaseErr("Incorrect cumulativeQuotaInterest")); - assertEq(enabledTokensMask, enabledTokensMaskBefore, _testCaseErr("Incorrect enabledTokensMask")); + assertEq(creditAccount, expectedAccount, _testCaseErr("Incorrect credit account returned")); - assertEq(lastDebtUpdate, 0, _testCaseErr("Incorrect lastDebtUpdate")); + (,, uint128 cumulativeQuotaInterest, uint128 quotaFees,, uint16 flags, uint64 lastDebtUpdate, address borrower) + = creditManager.creditAccountInfo(creditAccount); + assertEq(cumulativeQuotaInterest, 1, _testCaseErr("Incorrect cumulativeQuotaInterest")); + assertEq(quotaFees, 0, _testCaseErr("Incorrect quotaFees")); + assertEq(lastDebtUpdate, 0, _testCaseErr("Incorrect lastDebtUpdate")); assertEq(flags, 0, _testCaseErr("Incorrect flags")); assertEq(borrower, USER, _testCaseErr("Incorrect borrower")); - assertEq(creditManager.creditAccounts().length, 1, _testCaseErr("incorrect creditAccounts() length")); - assertEq(creditManager.creditAccounts()[0], creditAccount, _testCaseErr("incorrect creditAccounts()[0] value")); + assertEq(creditManager.creditAccountsLen(), 1, _testCaseErr("Incorerct creditAccounts length")); + assertEq(creditManager.creditAccounts()[0], creditAccount, _testCaseErr("Incorrect creditAccounts[0] value")); } - // // - // // - // // CLOSE CREDIT ACCOUNT - // // - // // + // + // + // CLOSE CREDIT ACCOUNT + // + // - /// @dev U:[CM-7]: close credit account reverts if account not exists - function test_U_CM_07_close_credit_account_reverts_if_account_not_exists() public creditManagerTest { - CollateralDebtData memory collateralDebtData; + /// @dev U:[CM-7]: close credit account works as expected + function test_U_CM_07_close_credit_account_works_as_expected() public creditManagerTest { address creditAccount = DUMB_ADDRESS; - vm.expectRevert(CreditAccountDoesNotExistException.selector); - creditManager.closeCreditAccount({ + creditManager.setCreditAccountInfoMap({ creditAccount: creditAccount, - closureAction: ClosureAction.CLOSE_ACCOUNT, - collateralDebtData: collateralDebtData, - payer: DUMB_ADDRESS, - to: DUMB_ADDRESS, - skipTokensMask: 0, - convertToETH: false + debt: 123, + cumulativeIndexLastUpdate: 0, + cumulativeQuotaInterest: 0, + quotaFees: 0, + enabledTokensMask: 0, + flags: 0, + borrower: address(0) }); - uint64 newBlock = 12312312; - - vm.roll(newBlock); - creditManager.setLastDebtUpdate(creditAccount, newBlock); - creditManager.setBorrower(creditAccount, USER); + vm.expectRevert(CloseAccountWithNonZeroDebtException.selector); + creditManager.closeCreditAccount(creditAccount); - vm.expectRevert(DebtUpdatedTwiceInOneBlockException.selector); - creditManager.closeCreditAccount({ + creditManager.addToCAList(creditAccount); + creditManager.setCreditAccountInfoMap({ creditAccount: creditAccount, - closureAction: ClosureAction.CLOSE_ACCOUNT, - collateralDebtData: collateralDebtData, - payer: DUMB_ADDRESS, - to: DUMB_ADDRESS, - skipTokensMask: 0, - convertToETH: false + debt: 0, + cumulativeIndexLastUpdate: 0, + cumulativeQuotaInterest: 0, + quotaFees: 0, + enabledTokensMask: 0, + flags: 123, + borrower: address(0) }); + + vm.expectCall(address(accountFactory), abi.encodeCall(accountFactory.returnCreditAccount, (creditAccount))); + creditManager.closeCreditAccount(creditAccount); + + (,,,, uint256 enabledTokensMask, uint16 flags, uint64 lastDebtUpdate, address borrower) = + creditManager.creditAccountInfo(creditAccount); + assertEq(enabledTokensMask, 0, "enabledTokensMask not cleared"); + assertEq(borrower, address(0), "borrower not cleared"); + assertEq(lastDebtUpdate, 0, "lastDebtUpadte not cleared"); + assertEq(flags, 0, "flags not cleared"); + + assertEq(creditManager.creditAccountsLen(), 0, _testCaseErr("incorrect creditAccounts length")); } - struct CloseCreditAccountTestCase { + // + // + // LIQUIDATE CREDIT ACCOUNT + // + // + struct LiquidateAccountTestCase { string name; - ClosureAction closureAction; uint256 debt; uint256 accruedInterest; uint256 accruedFees; @@ -608,12 +575,22 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH uint256 enabledTokensMask; address[] quotedTokens; uint256 underlyingBalance; + uint256 usdcBalance; + uint256 linkBalance; + bool isExpired; // EXPECTED + bool expectedRevert; + uint256 amountToLiquidator; + uint256 enabledTokensMaskAfter; bool expectedSetLimitsToZero; } - /// @dev U:[CM-8]: close credit account works as expected - function test_U_CM_08_close_credit_correctly_makes_payments() public withFeeTokenCase creditManagerTest { + uint256 constant USDC_MULTIPLIER = 2; + + uint256 constant LINK_MULTIPLIER = 4; + + /// @dev U:[CM-8]: liquidate credit account works as expected + function test_U_CM_08_liquidateCreditAccount_correctly_makes_payments() public withFeeTokenCase creditManagerTest { uint256 debt = DAI_ACCOUNT_AMOUNT; vm.assume(debt > 1_000); @@ -628,96 +605,146 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH hasQuotedTokens[0] = tokenTestSuite.addressOf(Tokens.USDC); hasQuotedTokens[1] = tokenTestSuite.addressOf(Tokens.LINK); - CloseCreditAccountTestCase[7] memory cases = [ - CloseCreditAccountTestCase({ - name: "Closure case for account with no pay from payer", - closureAction: ClosureAction.CLOSE_ACCOUNT, + priceOracleMock.setPrice(underlying, 10 ** 8); + + /// @notice sets price 2 USD for underlying + priceOracleMock.setPrice(tokenTestSuite.addressOf(Tokens.USDC), USDC_MULTIPLIER * 10 ** 8); + + /// @notice sets price 4 USD for underlying + priceOracleMock.setPrice(tokenTestSuite.addressOf(Tokens.LINK), LINK_MULTIPLIER * 10 ** 8); + + vm.startPrank(CONFIGURATOR); + creditManager.addToken(tokenTestSuite.addressOf(Tokens.USDC)); + creditManager.addToken(tokenTestSuite.addressOf(Tokens.LINK)); + vm.stopPrank(); + + uint256 LINK_TOKEN_MASK = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); + + LiquidateAccountTestCase[7] memory cases = [ + LiquidateAccountTestCase({ + name: "Liquidate account with profit, undelying only", debt: debt, accruedInterest: 0, accruedFees: 0, - totalValue: 0, + totalValue: debt * 2, enabledTokensMask: UNDERLYING_TOKEN_MASK, quotedTokens: hasQuotedTokens, - underlyingBalance: debt + 1, + underlyingBalance: debt * 2, + usdcBalance: 0, + linkBalance: 0, + isExpired: false, // EXPECTED + expectedRevert: false, + amountToLiquidator: debt * 2 * DEFAULT_LIQUIDATION_PREMIUM / PERCENTAGE_FACTOR, + enabledTokensMaskAfter: UNDERLYING_TOKEN_MASK, expectedSetLimitsToZero: false }), - CloseCreditAccountTestCase({ - name: "Closure case for account with with charging payer", - closureAction: ClosureAction.CLOSE_ACCOUNT, + LiquidateAccountTestCase({ + name: "Liquidate account with profit, undelying only, revert not enough remaing funds", debt: debt, accruedInterest: 0, accruedFees: 0, - totalValue: 0, + totalValue: debt * 2, enabledTokensMask: UNDERLYING_TOKEN_MASK, quotedTokens: hasQuotedTokens, - underlyingBalance: debt / 2, + underlyingBalance: _amountWithFee(debt + debt * 2 * DEFAULT_FEE_LIQUIDATION / PERCENTAGE_FACTOR), + usdcBalance: 0, + linkBalance: 0, + isExpired: false, // EXPECTED + expectedRevert: true, + amountToLiquidator: 0, + enabledTokensMaskAfter: UNDERLYING_TOKEN_MASK, expectedSetLimitsToZero: false }), - CloseCreditAccountTestCase({ - name: "Liquidate account with profit", - closureAction: ClosureAction.LIQUIDATE_ACCOUNT, + LiquidateAccountTestCase({ + name: "Liquidate account with profit, without quotas, with link, underlyingBalance is sent", debt: debt, accruedInterest: 0, accruedFees: 0, totalValue: debt * 2, - enabledTokensMask: UNDERLYING_TOKEN_MASK, - quotedTokens: hasQuotedTokens, - underlyingBalance: debt * 2, + enabledTokensMask: UNDERLYING_TOKEN_MASK | LINK_TOKEN_MASK, + quotedTokens: new address[](0), + underlyingBalance: _amountWithFee(debt + debt * 2 * DEFAULT_FEE_LIQUIDATION / PERCENTAGE_FACTOR) + 1_000, + usdcBalance: 0, + linkBalance: debt, + isExpired: false, // EXPECTED + expectedRevert: false, + amountToLiquidator: 1_000, + enabledTokensMaskAfter: UNDERLYING_TOKEN_MASK | LINK_TOKEN_MASK, expectedSetLimitsToZero: false }), - CloseCreditAccountTestCase({ - name: "Liquidate account with profit, liquidator pays, with quotedTokens", - closureAction: ClosureAction.LIQUIDATE_ACCOUNT, + LiquidateAccountTestCase({ + name: "Liquidate account with profit, without quotas, with link, underlyingBalance is sent, EXPIRED", debt: debt, accruedInterest: 0, accruedFees: 0, - totalValue: _amountWithFee(debt * 100 / 95), - enabledTokensMask: UNDERLYING_TOKEN_MASK, - quotedTokens: hasQuotedTokens, - underlyingBalance: 0, + totalValue: debt * 2, + enabledTokensMask: UNDERLYING_TOKEN_MASK | LINK_TOKEN_MASK, + quotedTokens: new address[](0), + underlyingBalance: _amountWithFee(debt + debt * 2 * DEFAULT_FEE_LIQUIDATION_EXPIRED / PERCENTAGE_FACTOR) + 1_000, + usdcBalance: 0, + linkBalance: debt, + isExpired: true, // EXPECTED + expectedRevert: false, + amountToLiquidator: 1_000, + enabledTokensMaskAfter: UNDERLYING_TOKEN_MASK | LINK_TOKEN_MASK, expectedSetLimitsToZero: false }), - CloseCreditAccountTestCase({ - name: "Liquidate account with loss, no quoted tokens", - closureAction: ClosureAction.LIQUIDATE_ACCOUNT, + LiquidateAccountTestCase({ + name: "Liquidate account with profit, with quotas, with link, underlyingBalance is sent", debt: debt, accruedInterest: 0, accruedFees: 0, - totalValue: debt / 2, - enabledTokensMask: UNDERLYING_TOKEN_MASK, - quotedTokens: new address[](0), - underlyingBalance: debt / 2, + totalValue: debt * 2, + enabledTokensMask: UNDERLYING_TOKEN_MASK | LINK_TOKEN_MASK, + quotedTokens: hasQuotedTokens, + underlyingBalance: _amountWithFee(debt + debt * 2 * DEFAULT_FEE_LIQUIDATION / PERCENTAGE_FACTOR) + 1_000, + usdcBalance: 0, + linkBalance: debt, + isExpired: false, // EXPECTED + expectedRevert: false, + amountToLiquidator: 1_000, + enabledTokensMaskAfter: UNDERLYING_TOKEN_MASK, expectedSetLimitsToZero: false }), - CloseCreditAccountTestCase({ - name: "Liquidate account with loss, with quotaTokens", - closureAction: ClosureAction.LIQUIDATE_ACCOUNT, + LiquidateAccountTestCase({ + name: "Liquidate account with loss, without quotaTokens, undelying only", debt: debt, accruedInterest: 0, accruedFees: 0, totalValue: debt / 2, enabledTokensMask: UNDERLYING_TOKEN_MASK, - quotedTokens: hasQuotedTokens, + quotedTokens: new address[](0), underlyingBalance: debt / 2, + usdcBalance: 0, + linkBalance: 0, + isExpired: false, // EXPECTED - expectedSetLimitsToZero: true + expectedRevert: false, + amountToLiquidator: debt / 2 * DEFAULT_LIQUIDATION_PREMIUM / PERCENTAGE_FACTOR, + enabledTokensMaskAfter: UNDERLYING_TOKEN_MASK, + expectedSetLimitsToZero: false }), - CloseCreditAccountTestCase({ - name: "Liquidate account with loss, with quotaTokens, Liquidator pays", - closureAction: ClosureAction.LIQUIDATE_ACCOUNT, + LiquidateAccountTestCase({ + name: "Liquidate account with loss, with quotaTokens, undelying only", debt: debt, accruedInterest: 0, accruedFees: 0, totalValue: debt / 2, enabledTokensMask: UNDERLYING_TOKEN_MASK, quotedTokens: hasQuotedTokens, - underlyingBalance: 0, + underlyingBalance: debt / 2, + usdcBalance: 0, + linkBalance: 0, + isExpired: false, // EXPECTED + expectedRevert: false, + amountToLiquidator: debt / 2 * DEFAULT_LIQUIDATION_PREMIUM / PERCENTAGE_FACTOR, + enabledTokensMaskAfter: UNDERLYING_TOKEN_MASK, expectedSetLimitsToZero: true }) ]; @@ -726,17 +753,10 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH creditManager.setBorrower(creditAccount, USER); - tokenTestSuite.mint({token: underlying, to: LIQUIDATOR, amount: _amountWithFee(debt * 2)}); - - vm.prank(LIQUIDATOR); - IERC20(underlying).approve({spender: address(creditManager), amount: type(uint256).max}); - - assertEq(accountFactory.returnedAccount(), address(0), "SETUP: returnAccount is already set"); - for (uint256 i; i < cases.length; ++i) { uint256 snapshot = vm.snapshot(); - CloseCreditAccountTestCase memory _case = cases[i]; + LiquidateAccountTestCase memory _case = cases[i]; caseName = string.concat(caseName, _case.name); @@ -748,294 +768,169 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH collateralDebtData.totalValue = _case.totalValue; collateralDebtData.enabledTokensMask = _case.enabledTokensMask; collateralDebtData.quotedTokens = _case.quotedTokens; - - /// @notice We do not test math correctness here, it could be found in lib test - /// We assume here, that lib is tested and provide correct results, the test checks - /// that te contract sends amout to correct addresses and implement another logic is need - uint256 amountToPool; - uint256 profit; - uint256 expectedRemainingFunds; - uint256 expectedLoss; - - if (_case.closureAction == ClosureAction.CLOSE_ACCOUNT) { - (amountToPool, profit) = collateralDebtData.calcClosePayments({amountWithFeeFn: _amountWithFee}); - } else { - (amountToPool, expectedRemainingFunds, profit, expectedLoss) = collateralDebtData - .calcLiquidationPayments({ - liquidationDiscount: _case.closureAction == ClosureAction.LIQUIDATE_ACCOUNT - ? PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM - : PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM_EXPIRED, - feeLiquidation: _case.closureAction == ClosureAction.LIQUIDATE_ACCOUNT - ? DEFAULT_FEE_LIQUIDATION - : DEFAULT_FEE_LIQUIDATION_EXPIRED, + { + /// @notice We do not test math correctness here, it could be found in lib test + /// We assume here, that lib is tested and provide correct results, the test checks + /// that te contract sends amout to correct addresses and implement another logic is need + uint256 amountToPool; + uint256 profit; + uint256 minRemainingFunds; + uint256 expectedLoss; + + (amountToPool, minRemainingFunds, profit, expectedLoss) = collateralDebtData.calcLiquidationPayments({ + liquidationDiscount: _case.isExpired + ? PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM_EXPIRED + : PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM, + feeLiquidation: _case.isExpired ? DEFAULT_FEE_LIQUIDATION_EXPIRED : DEFAULT_FEE_LIQUIDATION, amountWithFeeFn: _amountWithFee, amountMinusFeeFn: _amountMinusFee }); - } - tokenTestSuite.mint(underlying, creditAccount, _case.underlyingBalance); + tokenTestSuite.mint(underlying, creditAccount, _case.underlyingBalance); + tokenTestSuite.mint(tokenTestSuite.addressOf(Tokens.USDC), creditAccount, _case.usdcBalance); + tokenTestSuite.mint(tokenTestSuite.addressOf(Tokens.LINK), creditAccount, _case.linkBalance); - startTokenTrackingSession(caseName); + vm.startPrank(CONFIGURATOR); + for (uint256 i; i < _case.quotedTokens.length; ++i) { + creditManager.setQuotedMask( + creditManager.quotedTokensMask() | creditManager.getTokenMaskOrRevert(_case.quotedTokens[i]) + ); + } - expectTokenTransfer({ - reason: "debt transfer to pool", - token: underlying, - from: creditAccount, - to: address(poolMock), - amount: _amountMinusFee(amountToPool) - }); + vm.stopPrank(); + + collateralDebtData.quotedTokensMask = creditManager.quotedTokensMask(); + + startTokenTrackingSession(caseName); - if (_case.underlyingBalance < amountToPool + expectedRemainingFunds + 1) { expectTokenTransfer({ - reason: "payer to creditAccount", + reason: "debt transfer to pool", token: underlying, - from: LIQUIDATOR, - to: creditAccount, - amount: amountToPool + expectedRemainingFunds - _case.underlyingBalance + 1 + from: creditAccount, + to: address(poolMock), + amount: _amountMinusFee(amountToPool) }); - } else { - uint256 amount = _case.underlyingBalance - amountToPool - expectedRemainingFunds - 1; - if (amount > 1) { + + if (_case.amountToLiquidator != 0) { expectTokenTransfer({ reason: "transfer to caller", token: underlying, from: creditAccount, to: FRIEND, - amount: amount + amount: _case.amountToLiquidator }); } - } - - if (expectedRemainingFunds > 1) { - expectTokenTransfer({ - reason: "remaning funds to borrower", - token: underlying, - from: creditAccount, - to: USER, - amount: _amountMinusFee(expectedRemainingFunds) - }); - } - - uint256 poolBalanceBefore = IERC20(underlying).balanceOf(address(poolMock)); - - /// - /// CLOSE CREDIT ACC - /// - (uint256 remainingFunds, uint256 loss) = creditManager.closeCreditAccount({ - creditAccount: creditAccount, - closureAction: _case.closureAction, - collateralDebtData: collateralDebtData, - payer: LIQUIDATOR, - to: FRIEND, - skipTokensMask: 0, - convertToETH: false - }); - - assertEq(poolMock.repayAmount(), collateralDebtData.debt, _testCaseErr("Incorrect repay amount")); - assertEq(poolMock.repayProfit(), profit, _testCaseErr("Incorrect profit")); - assertEq(poolMock.repayLoss(), loss, _testCaseErr("Incorrect loss")); - - assertEq(remainingFunds, expectedRemainingFunds, _testCaseErr("incorrect remainingFunds")); - - assertEq(loss, expectedLoss, _testCaseErr("incorrect loss")); - - checkTokenTransfers({debug: false}); - - /// @notice Pool balance invariant keeps correct transfer to pool during closure - - expectBalance({ - token: underlying, - holder: address(poolMock), - expectedBalance: poolBalanceBefore + collateralDebtData.debt + collateralDebtData.accruedInterest + profit - - loss, - reason: "Pool balance invariant" - }); - (,,,,,,, address borrower) = creditManager.creditAccountInfo(creditAccount); - assertEq(borrower, address(0), "Borrowers wasn't cleared"); + uint256 poolBalanceBefore = IERC20(underlying).balanceOf(address(poolMock)); - assertEq( - poolQuotaKeeperMock.call_creditAccount(), - _case.quotedTokens.length == 0 ? address(0) : creditAccount, - "Incorrect creditAccount call to PQK" - ); + /// + /// CLOSE CREDIT ACC + /// - assertTrue( - poolQuotaKeeperMock.call_setLimitsToZero() == _case.expectedSetLimitsToZero, "Incorrect setLimitsToZero" - ); + if (_case.expectedRevert) { + vm.expectRevert(InsufficientRemainingFundsException.selector); + } else { + vm.expectCall( + address(poolMock), + abi.encodeCall(poolMock.repayCreditAccount, (_case.debt, profit, expectedLoss)) + ); + } - assertEq(accountFactory.returnedAccount(), creditAccount, "returnAccount wasn't called"); + (uint256 remainingFunds, uint256 loss) = creditManager.liquidateCreditAccount({ + creditAccount: creditAccount, + collateralDebtData: collateralDebtData, + to: FRIEND, + isExpired: _case.isExpired + }); - assertEq(creditManager.creditAccounts().length, 0, _testCaseErr("incorrect creditAccounts() length")); + if (_case.expectedRevert) return; - vm.revertTo(snapshot); - } - } - - /// @dev U:[CM-9]: close credit account works as expected - function test_U_CM_09_close_credit_transfers_tokens_correctly(uint256 skipTokenMask) - public - withFeeTokenCase - creditManagerTest - { - bool convertToEth = (skipTokenMask % 2) != 0; - uint8 numberOfTokens = uint8(skipTokenMask % 253); + assertEq(poolMock.repayAmount(), collateralDebtData.debt, _testCaseErr("Incorrect repay amount")); + assertEq(poolMock.repayProfit(), profit, _testCaseErr("Incorrect profit")); + assertEq(poolMock.repayLoss(), loss, _testCaseErr("Incorrect loss")); + { + uint256 expectedRemainingFunds = _case.underlyingBalance - amountToPool - _case.amountToLiquidator + + _case.usdcBalance * USDC_MULTIPLIER + _case.linkBalance * LINK_MULTIPLIER; + assertEq(remainingFunds, expectedRemainingFunds, _testCaseErr("incorrect remainingFunds")); + } - CollateralDebtData memory collateralDebtData; - collateralDebtData.debt = DAI_ACCOUNT_AMOUNT; + assertEq(loss, expectedLoss, _testCaseErr("incorrect loss")); - /// @notice `+2` for underlying and WETH token - collateralDebtData.enabledTokensMask = getHash(skipTokenMask, 2) & ((1 << (numberOfTokens + 2)) - 1); + checkTokenTransfers({debug: false}); - address creditAccount = accountFactory.usedAccount(); + /// @notice Pool balance invariant keeps correct transfer to pool during closure - creditManager.setBorrower(creditAccount, USER); - tokenTestSuite.mint({token: underlying, to: creditAccount, amount: _amountWithFee(collateralDebtData.debt * 2)}); - - address weth = tokenTestSuite.addressOf(Tokens.WETH); + expectBalance({ + token: underlying, + holder: address(poolMock), + expectedBalance: poolBalanceBefore + amountToPool, + reason: "Pool balance invariant" + }); - vm.startPrank(CONFIGURATOR); - creditManager.addToken(weth); - creditManager.setCollateralTokenData(weth, 8000, 8000, type(uint40).max, 0); + expectBalance({ + token: underlying, + holder: creditAccount, + expectedBalance: _case.underlyingBalance - amountToPool - _case.amountToLiquidator, + reason: "Credit account balance invariant" + }); - vm.stopPrank(); + assertEq( + poolQuotaKeeperMock.call_creditAccount(), + _case.quotedTokens.length == 0 ? address(0) : creditAccount, + _testCaseErr("Incorrect creditAccount call to PQK") + ); - { - uint256 randomAmount = skipTokenMask % DAI_ACCOUNT_AMOUNT; - tokenTestSuite.mint({token: weth, to: creditAccount, amount: randomAmount}); - _addTokensBatch({creditAccount: creditAccount, numberOfTokens: numberOfTokens, balance: randomAmount}); - } + assertTrue( + poolQuotaKeeperMock.call_setLimitsToZero() == _case.expectedSetLimitsToZero, + _testCaseErr("Incorrect setLimitsToZero") + ); + } - caseName = string.concat(caseName, "token transfer with ", Strings.toString(numberOfTokens), " on account"); + { + (uint256 debt,, uint128 cumulativeQuotaInterest, uint128 quotaFees,,,,) = + creditManager.creditAccountInfo(creditAccount); - startTokenTrackingSession(caseName); + assertEq(debt, 0, _testCaseErr("Debt is not zero")); + assertEq(cumulativeQuotaInterest, 1, _testCaseErr("cumulativeQuotaInterest is not 1")); + assertEq(quotaFees, 0, _testCaseErr("quotaFees is not zero")); + } - uint8 len = creditManager.collateralTokensCount(); + { + (,,,, uint256 enabledTokensMask,, uint64 lastDebtUpdate, address borrower) = + creditManager.creditAccountInfo(creditAccount); - /// @notice it starts from 1, because underlying token has index 0 - for (uint8 i = 0; i < len; ++i) { - uint256 tokenMask = 1 << i; - address token = creditManager.getTokenByMask(tokenMask); - uint256 balance = IERC20(token).balanceOf(creditAccount); - - if ( - (collateralDebtData.enabledTokensMask & tokenMask != 0) && (tokenMask & skipTokenMask == 0) - && (balance > 1) - ) { - if (i == 0) { - expectTokenTransfer({ - reason: "transfer underlying token ", - token: underlying, - from: creditAccount, - to: FRIEND, - amount: collateralDebtData.debt - 1 - }); - } else { - expectTokenTransfer({ - reason: string.concat("transfer token ", IERC20Metadata(token).symbol()), - token: token, - from: creditAccount, - to: (convertToEth && token == weth) ? address(withdrawalManagerMock) : FRIEND, - amount: balance - 1 - }); - } + assertEq(enabledTokensMask, _case.enabledTokensMaskAfter, _testCaseErr("Incorrect enabled tokensMask")); + assertEq(lastDebtUpdate, block.number, _testCaseErr("Incorrect lastDebtUpdate")); + assertEq(borrower, USER, _testCaseErr("Incorrect borrower after")); } - } - creditManager.closeCreditAccount({ - creditAccount: creditAccount, - closureAction: ClosureAction.CLOSE_ACCOUNT, - collateralDebtData: collateralDebtData, - payer: USER, - to: FRIEND, - skipTokensMask: skipTokenMask, - convertToETH: convertToEth - }); - - checkTokenTransfers({debug: true}); + vm.revertTo(snapshot); + } } - /// @dev U:[CM-9A]: close credit account transfers tokens correctly with zero debt - function test_U_CM_09A_close_credit_transfers_tokens_correctly_with_zero_debt(uint256 skipTokenMask) - public - withFeeTokenCase - creditManagerTest - { - bool convertToEth = (skipTokenMask % 2) != 0; - uint8 numberOfTokens = uint8(skipTokenMask % 253); - + /// @dev U:[CM-9]: liquidate credit account reverts if called twice a block + function test_U_CM_09_liquidateCreditAccount_reverts_if_called_twice_a_block() public creditManagerTest { CollateralDebtData memory collateralDebtData; - collateralDebtData.debt = 0; - - /// @notice `+2` for underlying and WETH token - collateralDebtData.enabledTokensMask = getHash(skipTokenMask, 2) & ((1 << (numberOfTokens + 2)) - 1); + collateralDebtData._poolQuotaKeeper = address(poolQuotaKeeperMock); + collateralDebtData.debt = DAI_ACCOUNT_AMOUNT; + collateralDebtData.accruedInterest = 0; + collateralDebtData.accruedFees = 0; + collateralDebtData.totalValue = DAI_ACCOUNT_AMOUNT * 2; + collateralDebtData.enabledTokensMask = UNDERLYING_TOKEN_MASK; address creditAccount = accountFactory.usedAccount(); - creditManager.setBorrower(creditAccount, USER); - tokenTestSuite.mint({token: underlying, to: creditAccount, amount: DAI_ACCOUNT_AMOUNT}); - - address weth = tokenTestSuite.addressOf(Tokens.WETH); - - vm.startPrank(CONFIGURATOR); - creditManager.addToken(weth); - creditManager.setCollateralTokenData(weth, 8000, 8000, type(uint40).max, 0); - - vm.stopPrank(); + tokenTestSuite.mint(underlying, creditAccount, DAI_ACCOUNT_AMOUNT * 2); - { - uint256 randomAmount = skipTokenMask % DAI_ACCOUNT_AMOUNT; - tokenTestSuite.mint({token: weth, to: creditAccount, amount: randomAmount}); - _addTokensBatch({creditAccount: creditAccount, numberOfTokens: numberOfTokens, balance: randomAmount}); - } - - caseName = string.concat(caseName, "token transfer with ", Strings.toString(numberOfTokens), " on account"); - - startTokenTrackingSession(caseName); - - uint8 len = creditManager.collateralTokensCount(); - - /// @notice it starts from 1, because underlying token has index 0 - for (uint8 i = 0; i < len; ++i) { - uint256 tokenMask = 1 << i; - address token = creditManager.getTokenByMask(tokenMask); - uint256 balance = IERC20(token).balanceOf(creditAccount); - - if ( - (collateralDebtData.enabledTokensMask & tokenMask != 0) && (tokenMask & skipTokenMask == 0) - && (balance > 1) - ) { - if (i == 0) { - expectTokenTransfer({ - reason: "transfer underlying token ", - token: underlying, - from: creditAccount, - to: FRIEND, - amount: _amountMinusFee(DAI_ACCOUNT_AMOUNT - 1) - }); - } else { - expectTokenTransfer({ - reason: string.concat("transfer token ", IERC20Metadata(token).symbol()), - token: token, - from: creditAccount, - to: (convertToEth && token == weth) ? address(withdrawalManagerMock) : FRIEND, - amount: balance - 1 - }); - } - } - } + creditManager.setLastDebtUpdate({creditAccount: creditAccount, lastDebtUpdate: uint64(block.number)}); - creditManager.closeCreditAccount({ + vm.expectRevert(DebtUpdatedTwiceInOneBlockException.selector); + creditManager.liquidateCreditAccount({ creditAccount: creditAccount, - closureAction: ClosureAction.CLOSE_ACCOUNT, collateralDebtData: collateralDebtData, - payer: USER, to: FRIEND, - skipTokensMask: skipTokenMask, - convertToETH: convertToEth + isExpired: false }); - - checkTokenTransfers({debug: true}); } // @@ -1388,136 +1283,35 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH assertEq(newDebt, expectedNewDebt, _testCaseErr("Incorrect new debt")); - assertEq(poolMock.repayAmount(), collateralDebtData.debt - newDebt, _testCaseErr("Incorrect repay amount")); - assertEq(poolMock.repayProfit(), expectedProfit, _testCaseErr("Incorrect repay profit")); - assertEq(poolMock.repayLoss(), 0, _testCaseErr("Incorrect repay loss")); - - /// @notice checking creditAccountInf update - { - (uint256 debt, uint256 cumulativeIndexLastUpdate, uint256 cumulativeQuotaInterest,,,,,) = - creditManager.creditAccountInfo(creditAccount); - - assertEq(debt, expectedNewDebt, _testCaseErr("Incorrect debt update in creditAccountInfo")); - assertEq( - cumulativeIndexLastUpdate, - expectedCumulativeIndex, - _testCaseErr("Incorrect cumulativeIndexLastUpdate update in creditAccountInfo") - ); - - /// @notice cumulativeQuotaInterest should not be changed if supportsQuotas == false - - assertEq( - cumulativeQuotaInterest, - expectedCumulativeQuotaInterest + 1, - _testCaseErr("Incorrect cumulativeQuotaInterest update in creditAccountInfo") - ); - } - - assertEq(tokensToEnable, 0, _testCaseErr("Incorrect tokensToEnable")); - - /// @notice it should disable token mask with 0 or 1 balance after - assertEq(tokensToDisable, amount != 0 ? UNDERLYING_TOKEN_MASK : 0, _testCaseErr("Incorrect tokensToDisable")); - } - - /// @dev U:[CM-11B]: manageDebt with full repayment is equivalent to closeCreditAccount - function test_U_CM_11B_manageDebt_full_repayment_equivalent_to_close_credit_account(uint256 _amount) - public - withFeeTokenCase - creditManagerTest - { - vm.assume(_amount < 10 ** 10 * (10 ** _decimals(underlying))); - vm.assume(_amount > 1); - - /// @notice for stack optimisation - uint256 amount = _amount; - - address creditAccount = accountFactory.usedAccount(); - - CollateralDebtData memory collateralDebtData; - - collateralDebtData.debt = amount * (amount % 5 + 1); - collateralDebtData.cumulativeIndexNow = RAY * (10 + amount % 5) / 10; - collateralDebtData.cumulativeIndexLastUpdate = RAY; - - collateralDebtData.accruedInterest = CreditLogic.calcAccruedInterest( - collateralDebtData.debt, collateralDebtData.cumulativeIndexLastUpdate, collateralDebtData.cumulativeIndexNow - ); - - collateralDebtData.cumulativeQuotaInterest = uint128(amount / (amount % 5 + 1)); - collateralDebtData.accruedInterest += collateralDebtData.cumulativeQuotaInterest; - - /// Quota fees - collateralDebtData.accruedFees += amount / (amount % 10 + 1); - - { - (uint16 feeInterest,,,,) = creditManager.fees(); - - collateralDebtData.accruedFees += (collateralDebtData.accruedInterest * feeInterest) / PERCENTAGE_FACTOR; - } - - poolMock.setCumulativeIndexNow(collateralDebtData.cumulativeIndexNow); - - uint256 initialCQI = collateralDebtData.cumulativeQuotaInterest + 1; - creditManager - /// @notice enabledTokensMask is read directly from function parameters, not from this function - .setCreditAccountInfoMap({ - creditAccount: creditAccount, - debt: collateralDebtData.debt, - cumulativeIndexLastUpdate: collateralDebtData.cumulativeIndexLastUpdate, - cumulativeQuotaInterest: uint128(initialCQI), - quotaFees: uint128(amount / (amount % 10 + 1)), - enabledTokensMask: 0, - flags: 0, - borrower: USER - }); - - { - uint256 amountOnAccount = _amountWithFee(collateralDebtData.calcTotalDebt()) + 1; - tokenTestSuite.mint(underlying, creditAccount, amountOnAccount); - } - - for (uint256 caseId = 0; caseId <= 1; ++caseId) { - uint256 ss = vm.snapshot(); - - caseName = string.concat(caseName, caseId == 0 ? "full repayment" : "close account"); - - startTokenTrackingSession(caseName); - - expectTokenTransfer({ - reason: "transfer from user to pool", - token: underlying, - from: creditAccount, - to: address(poolMock), - amount: collateralDebtData.calcTotalDebt() - }); + assertEq(poolMock.repayAmount(), collateralDebtData.debt - newDebt, _testCaseErr("Incorrect repay amount")); + assertEq(poolMock.repayProfit(), expectedProfit, _testCaseErr("Incorrect repay profit")); + assertEq(poolMock.repayLoss(), 0, _testCaseErr("Incorrect repay loss")); - if (caseId == 0) { - creditManager.manageDebt({ - creditAccount: creditAccount, - amount: type(uint256).max, - enabledTokensMask: 0, - action: ManageDebtAction.DECREASE_DEBT - }); - } else { - creditManager.closeCreditAccount({ - creditAccount: creditAccount, - closureAction: ClosureAction.CLOSE_ACCOUNT, - collateralDebtData: collateralDebtData, - payer: USER, - to: USER, - skipTokensMask: 0, - convertToETH: false - }); - } + /// @notice checking creditAccountInf update + { + (uint256 debt, uint256 cumulativeIndexLastUpdate, uint256 cumulativeQuotaInterest,,,,,) = + creditManager.creditAccountInfo(creditAccount); - checkTokenTransfers({debug: false}); + assertEq(debt, expectedNewDebt, _testCaseErr("Incorrect debt update in creditAccountInfo")); + assertEq( + cumulativeIndexLastUpdate, + expectedCumulativeIndex, + _testCaseErr("Incorrect cumulativeIndexLastUpdate update in creditAccountInfo") + ); - assertEq(poolMock.repayAmount(), collateralDebtData.debt, _testCaseErr("Incorrect repay amount")); - assertEq(poolMock.repayProfit(), collateralDebtData.accruedFees, _testCaseErr("Incorrect repay profit")); - assertEq(poolMock.repayLoss(), 0, _testCaseErr("Incorrect repay loss")); + /// @notice cumulativeQuotaInterest should not be changed if supportsQuotas == false - vm.revertTo(ss); + assertEq( + cumulativeQuotaInterest, + expectedCumulativeQuotaInterest + 1, + _testCaseErr("Incorrect cumulativeQuotaInterest update in creditAccountInfo") + ); } + + assertEq(tokensToEnable, 0, _testCaseErr("Incorrect tokensToEnable")); + + /// @notice it should disable token mask with 0 or 1 balance after + assertEq(tokensToDisable, amount != 0 ? UNDERLYING_TOKEN_MASK : 0, _testCaseErr("Incorrect tokensToDisable")); } /// @dev U:[CM-11C]: manageDebt reverts on full repayment with non-zero quotas @@ -1801,14 +1595,35 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH // // - /// @dev U:[CM-17]: fullCollateralCheck reverts if hf < 10K - function test_U_CM_17_fullCollateralCheck_reverts_if_hf_less_10K() public creditManagerTest { + /// @dev U:[CM-17]: fullCollateralCheck reverts with invalid params + function test_U_CM_17_fullCollateralCheck_reverts_with_invalid_params() public creditManagerTest { vm.expectRevert(CustomHealthFactorTooLowException.selector); creditManager.fullCollateralCheck({ creditAccount: DUMB_ADDRESS, enabledTokensMask: 0, collateralHints: new uint256[](0), - minHealthFactor: PERCENTAGE_FACTOR - 1 + minHealthFactor: PERCENTAGE_FACTOR - 1, + useSafePrices: false + }); + + uint256[] memory collateralHints = new uint256[](1); + vm.expectRevert(InvalidCollateralHintException.selector); + creditManager.fullCollateralCheck({ + creditAccount: DUMB_ADDRESS, + enabledTokensMask: 0, + collateralHints: collateralHints, + minHealthFactor: PERCENTAGE_FACTOR, + useSafePrices: false + }); + + collateralHints[0] = 3; + vm.expectRevert(InvalidCollateralHintException.selector); + creditManager.fullCollateralCheck({ + creditAccount: DUMB_ADDRESS, + enabledTokensMask: 0, + collateralHints: collateralHints, + minHealthFactor: PERCENTAGE_FACTOR, + useSafePrices: false }); } @@ -1850,7 +1665,7 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH }); CollateralDebtData memory collateralDebtData = - creditManager.calcDebtAndCollateralFC(creditAccount, CollateralCalcTask.DEBT_COLLATERAL_WITHOUT_WITHDRAWALS); + creditManager.calcDebtAndCollateralFC(creditAccount, CollateralCalcTask.DEBT_COLLATERAL); /// @notice fuzzler could find a combination which enabled tokens with zero balances, /// which cause to twvUSD == 0 and arithmetic errr later @@ -1870,7 +1685,8 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH creditAccount: creditAccount, enabledTokensMask: enabledTokensMask, collateralHints: new uint256[](0), - minHealthFactor: PERCENTAGE_FACTOR + minHealthFactor: PERCENTAGE_FACTOR, + useSafePrices: false }); assertTrue( @@ -1907,7 +1723,8 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH creditAccount: creditAccount, enabledTokensMask: enabledTokensMask, collateralHints: new uint256[](0), - minHealthFactor: PERCENTAGE_FACTOR + minHealthFactor: PERCENTAGE_FACTOR, + useSafePrices: false }); uint256 enabledTokensMaskAfter = creditManager.enabledTokensMaskOf(creditAccount); @@ -1955,7 +1772,7 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH }); CollateralDebtData memory collateralDebtData = - creditManager.calcDebtAndCollateralFC(creditAccount, CollateralCalcTask.DEBT_COLLATERAL_WITHOUT_WITHDRAWALS); + creditManager.calcDebtAndCollateralFC(creditAccount, CollateralCalcTask.DEBT_COLLATERAL); /// @notice fuzzler could find a combination which enabled tokens with zero balances, /// which cause to twvUSD == 0 and arithmetic errr later @@ -1977,7 +1794,8 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH creditAccount: creditAccount, enabledTokensMask: enabledTokensMask, collateralHints: new uint256[](0), - minHealthFactor: PERCENTAGE_FACTOR + minHealthFactor: PERCENTAGE_FACTOR, + useSafePrices: false }); uint256 enabledTokensMaskAfter = creditManager.enabledTokensMaskOf(creditAccount); @@ -2252,11 +2070,7 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH address creditAccount = DUMB_ADDRESS; - CollateralCalcTask[3] memory tasks = [ - CollateralCalcTask.DEBT_COLLATERAL_WITHOUT_WITHDRAWALS, - CollateralCalcTask.DEBT_COLLATERAL_CANCEL_WITHDRAWALS, - CollateralCalcTask.DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS - ]; + CollateralCalcTask[1] memory tasks = [CollateralCalcTask.DEBT_COLLATERAL]; for (uint256 taskIndex = 0; taskIndex < 1; ++taskIndex) { caseName = string.concat(caseName, _taskName(tasks[taskIndex])); @@ -2343,61 +2157,14 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH cumulativeQuotaInterest: uint128(vars.get("INITIAL_INTEREST") + 1), quotaFees: 0, enabledTokensMask: UNDERLYING_TOKEN_MASK, - flags: setFlag ? WITHDRAWAL_FLAG : 0, + flags: 0, borrower: USER }); - withdrawalManagerMock.setCancellableWithdrawals( - false, tokenTestSuite.addressOf(Tokens.LINK), amount1, tokenTestSuite.addressOf(Tokens.STETH), amount2 - ); - - withdrawalManagerMock.setCancellableWithdrawals( - true, tokenTestSuite.addressOf(Tokens.USDC), amount1, tokenTestSuite.addressOf(Tokens.DAI), amount2 - ); - CollateralDebtData memory collateralDebtData = creditManager.calcDebtAndCollateral({ creditAccount: creditAccount, - task: CollateralCalcTask.DEBT_COLLATERAL_WITHOUT_WITHDRAWALS - }); - - CollateralDebtData memory collateralDebtDataNormal = creditManager.calcDebtAndCollateral({ - creditAccount: creditAccount, - task: CollateralCalcTask.DEBT_COLLATERAL_CANCEL_WITHDRAWALS - }); - - CollateralDebtData memory collateralDebtDataForced = creditManager.calcDebtAndCollateral({ - creditAccount: creditAccount, - task: CollateralCalcTask.DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS + task: CollateralCalcTask.DEBT_COLLATERAL }); - - assertEq( - collateralDebtDataNormal.totalValueUSD - collateralDebtData.totalValueUSD, - setFlag ? amount1 * vars.get("LINK_PRICE") + amount2 * vars.get("STETH_PRICE") : 0, - _testCaseErr("Incorrect totalValueUSD normal case") - ); - - assertEq( - collateralDebtDataForced.totalValueUSD - collateralDebtData.totalValueUSD, - setFlag ? amount1 * vars.get("USDC_PRICE") + amount2 * vars.get("UNDERLYING_PRICE") : 0, - _testCaseErr("Incorrect totalValueUSD force case") - ); - - assertEq( - collateralDebtDataNormal.totalValue - collateralDebtData.totalValue, - setFlag - ? (amount1 * vars.get("LINK_PRICE") + amount2 * vars.get("STETH_PRICE")) / vars.get("UNDERLYING_PRICE") - : 0, - _testCaseErr("Incorrect totalValue normal case") - ); - - assertEq( - collateralDebtDataForced.totalValue - collateralDebtData.totalValue, - setFlag - ? (amount1 * vars.get("USDC_PRICE") + amount2 * vars.get("UNDERLYING_PRICE")) - / vars.get("UNDERLYING_PRICE") - : 0, - _testCaseErr("Incorrect totalValue force case") - ); } } @@ -2595,31 +2362,21 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH // SCHEDULE WITHDRAWAL // - /// @dev U:[CM-26]: scheduleWithdrawal reverts for unknown token - function test_U_CM_26_scheduleWithdrawal_reverts_for_unknown_token() public creditManagerTest { + /// @dev U:[CM-26]: withdrawCollateral reverts for unknown token + function test_U_CM_26_withdrawCollateral_reverts_for_unknown_token() public creditManagerTest { address creditAccount = DUMB_ADDRESS; address linkToken = tokenTestSuite.addressOf(Tokens.LINK); /// @notice check that it reverts on unknown token vm.expectRevert(TokenNotAllowedException.selector); - creditManager.scheduleWithdrawal({creditAccount: creditAccount, token: linkToken, amount: 20000}); + creditManager.withdrawCollateral({creditAccount: creditAccount, token: linkToken, amount: 20000, to: USER}); } - /// @dev U:[CM-27]: scheduleWithdrawal transfers token if delay == 0 - function test_U_CM_27_scheduleWithdrawal_transfers_token_if_delay_is_zero() - public - withFeeTokenCase - creditManagerTest - { + /// @dev U:[CM-27]: withdrawCollateral transfers token + function test_U_CM_27_withdrawCollateral_transfers_token() public withFeeTokenCase creditManagerTest { address creditAccount = address(new CreditAccountMock()); - withdrawalManagerMock.setDelay(0); - tokenTestSuite.mint(underlying, creditAccount, DAI_ACCOUNT_AMOUNT); - vm.expectRevert(CreditAccountDoesNotExistException.selector); - (uint256 tokensToDisable) = - creditManager.scheduleWithdrawal({creditAccount: creditAccount, token: underlying, amount: 20_000}); - creditManager.setBorrower({creditAccount: creditAccount, borrower: USER}); string memory caseNameBak = string.concat(caseName, "a part of funds"); @@ -2633,8 +2390,12 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH amount: _amountMinusFee(20_000) }); - (tokensToDisable) = - creditManager.scheduleWithdrawal({creditAccount: creditAccount, token: underlying, amount: 20_000}); + (uint256 tokensToDisable) = creditManager.withdrawCollateral({ + creditAccount: creditAccount, + token: underlying, + amount: 20_000, + to: USER + }); checkTokenTransfers({debug: false}); @@ -2655,330 +2416,16 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH amount: _amountMinusFee(amount) }); - (tokensToDisable) = - creditManager.scheduleWithdrawal({creditAccount: creditAccount, token: underlying, amount: amount}); - - checkTokenTransfers({debug: false}); - - assertEq(tokensToDisable, UNDERLYING_TOKEN_MASK, _testCaseErr("Incorrect token to disable")); - } - - /// @dev U:[CM-28]: scheduleWithdrawal works correctly if delay != 0 - function test_U_CM_28_scheduleWithdrawal_works_correctly_if_delay_not_eq_zero() - public - withFeeTokenCase - creditManagerTest - { - address creditAccount = address(new CreditAccountMock()); - - withdrawalManagerMock.setDelay(uint40(3 days)); - - tokenTestSuite.mint(underlying, creditAccount, DAI_ACCOUNT_AMOUNT); - - creditManager.setBorrower({creditAccount: creditAccount, borrower: USER}); - - string memory caseNameBak = string.concat(caseName, "a part of funds"); - startTokenTrackingSession(caseName); - - uint256 amountDelivered = _amountMinusFee(20_000); - - expectTokenTransfer({ - reason: "direct transfer to withdrawal manager", - token: underlying, - from: creditAccount, - to: address(withdrawalManagerMock), - amount: amountDelivered - }); - - vm.expectCall( - address(withdrawalManagerMock), - abi.encodeCall(IWithdrawalManagerV3.addScheduledWithdrawal, (creditAccount, underlying, amountDelivered, 0)) - ); - - (uint256 tokensToDisable) = - creditManager.scheduleWithdrawal({creditAccount: creditAccount, token: underlying, amount: 20_000}); - - checkTokenTransfers({debug: false}); - - assertEq(tokensToDisable, 0, _testCaseErr("Incorrect token to disable")); - - // KEEP 1 CASE DISABLES TOKEN - - caseName = string.concat(caseNameBak, " keep 1 token"); - uint256 amount = IERC20(underlying).balanceOf(creditAccount) - 1; - - (tokensToDisable) = - creditManager.scheduleWithdrawal({creditAccount: creditAccount, token: underlying, amount: amount}); - - assertEq(tokensToDisable, UNDERLYING_TOKEN_MASK, _testCaseErr("Incorrect token to disable")); - } - - /// @dev U:[CM-29]: claimWithdrawals works correctly - function test_U_CM_29_claimWithdrawals_works_correctly() public creditManagerTest { - address creditAccount = DUMB_ADDRESS; - - uint256 tokenMask = 5 << 1; - - /// @notice it does nothing if flag is not set - creditManager.setFlagFor(creditAccount, WITHDRAWAL_FLAG, false); - creditManager.claimWithdrawals(creditAccount, FRIEND, ClaimAction.CLAIM); - - assertTrue( - !withdrawalManagerMock.claimScheduledWithdrawalsWasCalled(), "Unexpected call to claimScheduledWithdrawals" - ); - - string memory caseNameBak = caseName; - - for (uint256 i = 0; i < 2; ++i) { - creditManager.setFlagFor(creditAccount, WITHDRAWAL_FLAG, true); - - bool hasScheduled = i == 1; - - caseName = string.concat(caseNameBak, "hasScheduled = ", hasScheduled ? "true" : "false"); - - withdrawalManagerMock.setClaimScheduledWithdrawals(hasScheduled, tokenMask); - (uint256 tokensToEnable) = creditManager.claimWithdrawals(creditAccount, FRIEND, ClaimAction.CLAIM); - - assertTrue( - creditManager.hasWithdrawals(creditAccount) == hasScheduled, - _testCaseErr("Incorrect WITHDRAWALS FLAG setting") - ); - assertEq(tokensToEnable, tokenMask, _testCaseErr("Incorrect tokensToEnable")); - } - } - - struct getCancellableWithdrawalsValueTestCase { - string name; - uint256 amount1; - uint256 amount2; - uint256 expectedTotalValueUSD; - } - - /// @dev U:[CM-30]: _getCancellableWithdrawalsValue works correctly - function test_U_CM_30_getCancellableWithdrawalsValue_works_correctly() public creditManagerTest { - priceOracleMock.setPrice(tokenTestSuite.addressOf(Tokens.DAI), 1 * (10 ** 8)); - priceOracleMock.setPrice(tokenTestSuite.addressOf(Tokens.WETH), 2_000 * (10 ** 8)); - - getCancellableWithdrawalsValueTestCase[4] memory cases = [ - getCancellableWithdrawalsValueTestCase({ - name: "amount1 == 0, amount2 == 0", - amount1: 0, - amount2: 0, - expectedTotalValueUSD: 0 - }), - getCancellableWithdrawalsValueTestCase({ - name: "amount1 == 100, amount2 == 0", - amount1: 100, - amount2: 0, - expectedTotalValueUSD: 100 * 1 - }), - getCancellableWithdrawalsValueTestCase({ - name: "amount1 == 0, amount2 == 20", - amount1: 0, - amount2: 20, - expectedTotalValueUSD: 20 * 2_000 - }), - getCancellableWithdrawalsValueTestCase({ - name: "amount1 == 15_00, amount2 == 8", - amount1: 15_00, - amount2: 8, - expectedTotalValueUSD: 15_00 * 1 + 8 * 20_00 - }) - ]; - - address creditAccount = DUMB_ADDRESS; - - for (uint256 j = 0; j < 2; ++j) { - bool isForceCancel = j == 1; - - for (uint256 i; i < cases.length; ++i) { - uint256 snapshot = vm.snapshot(); - - getCancellableWithdrawalsValueTestCase memory _case = cases[i]; - - caseName = string.concat(caseName, _case.name, ", isForceCancel = ", isForceCancel ? "true" : "false"); - - priceOracleMock.setRevertOnGetPrice(tokenTestSuite.addressOf(Tokens.DAI), _case.amount1 == 0); - priceOracleMock.setRevertOnGetPrice(tokenTestSuite.addressOf(Tokens.WETH), _case.amount2 == 0); - - withdrawalManagerMock.setCancellableWithdrawals({ - isForceCancel: isForceCancel, - token1: tokenTestSuite.addressOf(Tokens.DAI), - amount1: _case.amount1, - token2: tokenTestSuite.addressOf(Tokens.WETH), - amount2: _case.amount2 - }); - - vm.expectCall( - address(withdrawalManagerMock), - abi.encodeCall(IWithdrawalManagerV3.cancellableScheduledWithdrawals, (creditAccount, isForceCancel)) - ); - - uint256 totalValueUSD = - creditManager.getCancellableWithdrawalsValue(address(priceOracleMock), creditAccount, isForceCancel); - - assertEq(totalValueUSD, _case.expectedTotalValueUSD, _testCaseErr("Incorrect totalValueUSD")); - - vm.revertTo(snapshot); - } - } - } - - // - // - // TOKEN TRANSFER HELPERS - // - // - - // - // BATCH TOKEN TRANSFER - // - - /// @dev U:[CM-31]: batchTokensTransfer works correctly - function test_U_CM_31_batchTokensTransfer_works_correctly(uint256 tokensToTransferMask) - public - withFeeTokenCase - creditManagerTest - { - bool convertToEth = (uint256(keccak256(abi.encode((tokensToTransferMask)))) % 2) != 0; - uint8 numberOfTokens = uint8(tokensToTransferMask % 253); - - /// @notice `+2` for underlying and WETH token - tokensToTransferMask &= (1 << (numberOfTokens + 2)) - 1; - - address creditAccount = address(new CreditAccountMock()); - address weth = tokenTestSuite.addressOf(Tokens.WETH); - - vm.startPrank(CONFIGURATOR); - creditManager.addToken(weth); - creditManager.setCollateralTokenData(weth, 8000, 8000, type(uint40).max, 0); - - vm.stopPrank(); - - { - tokenTestSuite.mint({ - token: underlying, - to: creditAccount, - amount: uint256(keccak256(abi.encode((tokensToTransferMask)))) % type(uint192).max - }); - uint256 randomAmount = tokensToTransferMask % DAI_ACCOUNT_AMOUNT; - tokenTestSuite.mint({token: weth, to: creditAccount, amount: randomAmount}); - _addTokensBatch({creditAccount: creditAccount, numberOfTokens: numberOfTokens, balance: randomAmount}); - } - - caseName = string.concat(caseName, "token transfer with ", Strings.toString(numberOfTokens), " on account"); - - startTokenTrackingSession(caseName); - - uint8 len = creditManager.collateralTokensCount(); - - for (uint8 i = 0; i < len; ++i) { - uint256 tokenMask = 1 << i; - address token = creditManager.getTokenByMask(tokenMask); - uint256 balance = IERC20(token).balanceOf(creditAccount); - - if ((tokensToTransferMask & tokenMask != 0) && (balance > 1)) { - expectTokenTransfer({ - reason: string.concat("transfer token ", IERC20Metadata(token).symbol()), - token: token, - from: creditAccount, - to: (convertToEth && token == weth) ? address(withdrawalManagerMock) : FRIEND, - amount: (tokenMask == UNDERLYING_TOKEN_MASK) ? _amountMinusFee(balance - 1) : balance - 1 - }); - } - } - - creditManager.batchTokensTransfer({ - creditAccount: creditAccount, - to: FRIEND, - convertToETH: convertToEth, - tokensToTransferMask: tokensToTransferMask - }); - - checkTokenTransfers({debug: false}); - } - - // - // SAFE TOKEN TRANSFER - // - - /// @dev U:[CM-32]: safeTokenTransfer works correctly no revert case - function test_U_CM_32_safeTokenTransfer_works_correctly_no_revert_case() public creditManagerTest { - address weth = tokenTestSuite.addressOf(Tokens.WETH); - - uint256 amount = 22423423; - - for (uint256 i; i < 2; ++i) { - bool convertToEth = i == 1; - - caseName = string.concat(caseName, ", convertToEth =", convertToEth ? "true" : "false"); - - address creditAccount = address(new CreditAccountMock()); - tokenTestSuite.mint({token: weth, to: creditAccount, amount: amount}); - - startTokenTrackingSession(caseName); - - expectTokenTransfer({ - reason: "transfer token ", - token: weth, - from: creditAccount, - to: convertToEth ? address(withdrawalManagerMock) : FRIEND, - amount: amount - }); - - if (convertToEth) { - vm.expectCall( - address(withdrawalManagerMock), - abi.encodeCall(IWithdrawalManagerV3.addImmediateWithdrawal, (weth, address(this), amount)) - ); - } - - creditManager.safeTokenTransfer({ - creditAccount: creditAccount, - token: weth, - to: FRIEND, - amount: amount, - convertToETH: convertToEth - }); - - checkTokenTransfers({debug: false}); - } - } - - /// @dev U:[CM-33]: batchTokensTransfer works correctly - function test_U_CM_33_batchTokensTransfer_works_correctly() public withFeeTokenCase creditManagerTest { - uint256 amount = 22423423; - CreditAccountMock ca = new CreditAccountMock(); - ca.setRevertOnTransfer(underlying, FRIEND); - - address creditAccount = address(ca); - - tokenTestSuite.mint({token: underlying, to: creditAccount, amount: amount}); - - startTokenTrackingSession(caseName); - - expectTokenTransfer({ - reason: "transfer token ", - token: underlying, - from: creditAccount, - to: address(withdrawalManagerMock), - amount: _amountMinusFee(amount) - }); - - vm.expectCall( - address(withdrawalManagerMock), - abi.encodeCall(IWithdrawalManagerV3.addImmediateWithdrawal, (underlying, FRIEND, _amountMinusFee(amount))) - ); - - creditManager.safeTokenTransfer({ + (tokensToDisable) = creditManager.withdrawCollateral({ creditAccount: creditAccount, token: underlying, - to: FRIEND, amount: amount, - convertToETH: false + to: USER }); checkTokenTransfers({debug: false}); + + assertEq(tokensToDisable, UNDERLYING_TOKEN_MASK, _testCaseErr("Incorrect token to disable")); } // @@ -3059,10 +2506,6 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH uint16 flagToTest = uint16(1 << i); creditManager.setFlagFor(creditAccount, flagToTest, value); assertEq(creditManager.flagsOf(creditAccount) & flagToTest != 0, value, "Incorrect flag set"); - - if (flagToTest == WITHDRAWAL_FLAG) { - assertEq(creditManager.hasWithdrawals(creditAccount), value, "Incorrect hasWithdrawals"); - } } } } @@ -3089,7 +2532,7 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH vm.prank(CONFIGURATOR); creditManager.setMaxEnabledTokens(maxEnabledTokens); - if (mask.calcEnabledTokens() > maxEnabledTokens) { + if (mask.disable(UNDERLYING_TOKEN_MASK).calcEnabledTokens() > maxEnabledTokens) { vm.expectRevert(TooManyEnabledTokensException.selector); creditManager.saveEnabledTokensMask(creditAccount, mask); } else { diff --git a/contracts/test/unit/credit/CreditManagerV3Harness.sol b/contracts/test/unit/credit/CreditManagerV3Harness.sol index 419e7c36..552c4956 100644 --- a/contracts/test/unit/credit/CreditManagerV3Harness.sol +++ b/contracts/test/unit/credit/CreditManagerV3Harness.sol @@ -67,18 +67,6 @@ contract CreditManagerV3Harness is CreditManagerV3 { creditAccountInfo[creditAccount].borrower = borrower; } - function batchTokensTransfer(address creditAccount, address to, bool convertToETH, uint256 tokensToTransferMask) - external - { - _batchTokensTransfer(creditAccount, to, convertToETH, tokensToTransferMask); - } - - function safeTokenTransfer(address creditAccount, address token, address to, uint256 amount, bool convertToETH) - external - { - _safeTokenTransfer(creditAccount, token, to, amount, convertToETH); - } - function collateralTokenByMaskCalcLT(uint256 tokenMask, bool calcLT) external view @@ -100,14 +88,11 @@ contract CreditManagerV3Harness is CreditManagerV3 { enabledTokensMask: enabledTokensMaskOf(creditAccount), collateralHints: collateralHints, minHealthFactor: PERCENTAGE_FACTOR, - task: task + task: task, + useSafePrices: false }); } - function hasWithdrawals(address creditAccount) external view returns (bool) { - return _hasWithdrawals(creditAccount); - } - function saveEnabledTokensMask(address creditAccount, uint256 enabledTokensMask) external { _saveEnabledTokensMask(creditAccount, enabledTokensMask); } @@ -130,14 +115,6 @@ contract CreditManagerV3Harness is CreditManagerV3 { return _getQuotedTokensData(creditAccount, enabledTokensMask, collateralHints, _poolQuotaKeeper); } - function getCancellableWithdrawalsValue(address _priceOracle, address creditAccount, bool isForceCancel) - external - view - returns (uint256 totalValueUSD) - { - return _getCancellableWithdrawalsValue(_priceOracle, creditAccount, isForceCancel); - } - function getCollateralTokensData(uint256 tokenMask) external view returns (CollateralTokenData memory) { return collateralTokensData[tokenMask]; } diff --git a/contracts/test/unit/governance/Gauge.unit.t.sol b/contracts/test/unit/governance/Gauge.unit.t.sol index cd033015..a33a9998 100644 --- a/contracts/test/unit/governance/Gauge.unit.t.sol +++ b/contracts/test/unit/governance/Gauge.unit.t.sol @@ -59,11 +59,15 @@ contract GauageTest is TestHelper, IGaugeV3Events { gearStakingMock = new GearStakingMock(); gearStakingMock.setCurrentEpoch(900); - gauge = new GaugeV3Harness(address(poolMock), address(gearStakingMock)); + gauge = new GaugeV3Harness(address(poolMock), address(gearStakingMock)); } /// @dev U:[GA-01]: constructor sets correct values function test_U_GA_01_constructor_sets_correct_values() public { + vm.expectEmit(false, false, false, true); + emit SetFrozenEpoch(true); + gauge = new GaugeV3Harness(address(poolMock), address(gearStakingMock)); + assertEq(gauge.pool(), address(poolMock), "Incorrect pool"); assertEq(gauge.voter(), address(gearStakingMock), "Incorrect voter"); assertEq(gauge.epochLastUpdate(), 900, "Incorrect epoch"); diff --git a/contracts/test/unit/governance/GearStaking.t.sol b/contracts/test/unit/governance/GearStaking.t.sol index a1f88edb..872061bb 100644 --- a/contracts/test/unit/governance/GearStaking.t.sol +++ b/contracts/test/unit/governance/GearStaking.t.sol @@ -411,14 +411,10 @@ contract GearStakingTest is Test, IGearStakingV3Events { gearStaking.migrate(uint96(WAD / 2), new MultiVote[](0), new MultiVote[](0)); vm.prank(CONFIGURATOR); - gearStaking.setSuccessor(address(gearStakingSuccessor)); - - vm.expectRevert(CallerNotMigratorException.selector); - vm.prank(USER); - gearStaking.migrate(uint96(WAD / 2), new MultiVote[](0), new MultiVote[](0)); + gearStakingSuccessor.setMigrator(address(gearStaking)); vm.prank(CONFIGURATOR); - gearStakingSuccessor.setMigrator(address(gearStaking)); + gearStaking.setSuccessor(address(gearStakingSuccessor)); address newVotingContract = address(new TargetContractMock()); @@ -469,6 +465,14 @@ contract GearStakingTest is Test, IGearStakingV3Events { vm.expectRevert(CallerNotConfiguratorException.selector); gearStaking.setSuccessor(DUMB_ADDRESS); + vm.mockCall(DUMB_ADDRESS, abi.encodeWithSignature("migrator()"), abi.encode(address(0))); + + vm.expectRevert(IncompatibleSuccessorException.selector); + vm.prank(CONFIGURATOR); + gearStaking.setSuccessor(DUMB_ADDRESS); + + vm.mockCall(DUMB_ADDRESS, abi.encodeWithSignature("migrator()"), abi.encode(address(gearStaking))); + vm.expectEmit(true, false, false, false); emit SetSuccessor(DUMB_ADDRESS); diff --git a/contracts/test/unit/libraries/BalancesLogic.t.sol b/contracts/test/unit/libraries/BalancesLogic.t.sol index 61c9b986..77c4872e 100644 --- a/contracts/test/unit/libraries/BalancesLogic.t.sol +++ b/contracts/test/unit/libraries/BalancesLogic.t.sol @@ -6,13 +6,13 @@ pragma solidity ^0.8.17; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Balance} from "@gearbox-protocol/core-v2/contracts/libraries/Balances.sol"; -import {BalancesLogic, BalanceDelta, BalanceWithMask} from "../../../libraries/BalancesLogic.sol"; +import {BalancesLogic, BalanceDelta, BalanceWithMask, Comparison} from "../../../libraries/BalancesLogic.sol"; import {TestHelper} from "../../lib/helper.sol"; -/// @title BalancesLogic test -/// @notice [BM]: Unit tests for BalancesLogic -contract BalancesLogicTest is TestHelper { +/// @title Balances logic library unit test +/// @notice U:[BLL]: Unit tests for balances logic library +contract BalancesLogicUnitTest is TestHelper { address creditAccount; address[16] tokens; mapping(uint256 => uint256) maskToIndex; @@ -27,8 +27,23 @@ contract BalancesLogicTest is TestHelper { } } - /// @notice U:[BLL-1]: storeBalances works correctly - function test_BLL_01_storeBalances_works_correctly( + /// @notice U:[BLL-1]: `checkBalance` works correctly + function test_U_BLL_01_checkBalance_works_correctly(uint128[16] calldata balances, uint128 value, bool greater) + public + { + _setupTokenBalances(balances, 1); + + bool result = + BalancesLogic.checkBalance(creditAccount, tokens[0], value, greater ? Comparison.GREATER : Comparison.LESS); + if (greater) { + assertEq(result, balances[0] >= value); + } else { + assertEq(result, balances[0] <= value); + } + } + + /// @notice U:[BLL-2]: `storeBalances` with deltas works correctly + function test_U_BLL_02_storeBalances_with_deltas_works_correctly( uint128[16] calldata balances, int128[16] calldata deltas, uint256 length @@ -61,11 +76,12 @@ contract BalancesLogicTest is TestHelper { } } - /// @notice U:[BLL-2]: compareBalances works correctly - function test_BLL_02_compareBalances_works_correctly( + /// @notice U:[BLL-3]: `compareBalances` without tokens mask works correctly + function test_U_BLL_03_compareBalances_without_tokens_mask_works_correctly( uint128[16] calldata balances, uint128[16] calldata expectedBalances, - uint256 length + uint256 length, + bool greater ) public { vm.assume(length <= 16); @@ -73,84 +89,88 @@ contract BalancesLogicTest is TestHelper { bool expectedResult = true; for (uint256 i = 0; i < length; ++i) { - if (expectedBalances[i] > balances[i]) { + if (greater && expectedBalances[i] > balances[i]) { + expectedResult = false; + break; + } + + if (!greater && expectedBalances[i] < balances[i]) { expectedResult = false; break; } } - Balance[] memory expectedArray = new Balance[](length); + Balance[] memory storedBalances = new Balance[](length); for (uint256 i = 0; i < length; ++i) { - expectedArray[i] = Balance({token: tokens[i], balance: expectedBalances[i]}); + storedBalances[i] = Balance({token: tokens[i], balance: expectedBalances[i]}); } - bool result = BalancesLogic.compareBalances(creditAccount, expectedArray); + bool result = + BalancesLogic.compareBalances(creditAccount, storedBalances, greater ? Comparison.GREATER : Comparison.LESS); assertEq(result, expectedResult, "Incorrect result"); } - /// @notice U:[BLL-3]: storeForbiddenBalances works correctly - function test_BLL_03_storeForbiddenBalances_works_correctly( + /// @notice U:[BLL-4]: `storeBalances` with tokens mask works correctly + function test_U_BLL_04_storeBalances_with_tokens_mask_works_correctly( uint128[16] calldata balances, - uint256 enabledTokensMask, - uint256 forbiddenTokensMask + uint256 tokensMask ) public { - enabledTokensMask %= (2 ** 16); - forbiddenTokensMask %= (2 ** 16); + tokensMask %= (2 ** 16); _setupTokenBalances(balances, 16); - BalanceWithMask[] memory forbiddenBalances = - BalancesLogic.storeForbiddenBalances(creditAccount, enabledTokensMask, forbiddenTokensMask, _getTokenByMask); + BalanceWithMask[] memory storedBalances = + BalancesLogic.storeBalances(creditAccount, tokensMask, _getTokenByMask); uint256 j; for (uint256 i = 0; i < 16; ++i) { uint256 tokenMask = 1 << i; - if (tokenMask & enabledTokensMask & forbiddenTokensMask > 0) { - assertEq(forbiddenBalances[j].balance, balances[i], "Incorrect forbidden token balance"); + if (tokenMask & tokensMask > 0) { + assertEq(storedBalances[j].balance, balances[i], "Incorrect token balance"); - assertEq(forbiddenBalances[j].token, tokens[i], "Incorrect forbidden token address"); + assertEq(storedBalances[j].token, tokens[i], "Incorrect token address"); - assertEq(forbiddenBalances[j].tokenMask, tokenMask, "Incorrect forbidden token mask"); + assertEq(storedBalances[j].tokenMask, tokenMask, "Incorrect token mask"); ++j; } } } - /// @notice U:[BLL-4]: checkForbiddenBalances works correctly - function test_BLL_04_storeForbiddenBalances_works_correctly( + /// @notice U:[BLL-5]: `compareBalances` with tokens mask works correctly + function test_U_BLL_05_compareBalances_with_tokens_mask_works_correctly( uint128[16] calldata balancesBefore, uint128[16] calldata balancesAfter, - uint256 enabledTokensMaskBefore, - uint256 enabledTokensMaskAfter, - uint256 forbiddenTokensMask + uint256 tokensMask, + bool greater ) public { - enabledTokensMaskBefore %= (2 ** 16); - enabledTokensMaskAfter %= (2 ** 16); - forbiddenTokensMask %= (2 ** 16); + tokensMask %= (2 ** 16); _setupTokenBalances(balancesBefore, 16); - BalanceWithMask[] memory forbiddenBalances = BalancesLogic.storeForbiddenBalances( - creditAccount, enabledTokensMaskBefore, forbiddenTokensMask, _getTokenByMask - ); + BalanceWithMask[] memory storedBalances = + BalancesLogic.storeBalances(creditAccount, tokensMask, _getTokenByMask); _setupTokenBalances(balancesAfter, 16); bool expectedResult = true; - if ((enabledTokensMaskAfter & ~enabledTokensMaskBefore) & forbiddenTokensMask > 0) expectedResult = false; - for (uint256 i = 0; i < 16; ++i) { uint256 tokenMask = 1 << i; - if ((enabledTokensMaskAfter & forbiddenTokensMask & tokenMask > 0) && balancesAfter[i] > balancesBefore[i]) - { - expectedResult = false; - break; + if (tokensMask & tokenMask > 0) { + if (greater && balancesAfter[i] < balancesBefore[i]) { + expectedResult = false; + break; + } + + if (!greater && balancesAfter[i] > balancesBefore[i]) { + expectedResult = false; + break; + } } } - bool result = BalancesLogic.checkForbiddenBalances( - creditAccount, enabledTokensMaskBefore, enabledTokensMaskAfter, forbiddenBalances, forbiddenTokensMask + bool result = BalancesLogic.compareBalances( + creditAccount, tokensMask, storedBalances, greater ? Comparison.GREATER : Comparison.LESS ); assertEq(result, expectedResult, "Incorrect result"); } diff --git a/contracts/test/unit/libraries/CreditLogic.t.sol b/contracts/test/unit/libraries/CreditLogic.t.sol index 15d89570..1bfd5a14 100644 --- a/contracts/test/unit/libraries/CreditLogic.t.sol +++ b/contracts/test/unit/libraries/CreditLogic.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.17; import {CreditLogic} from "../../../libraries/CreditLogic.sol"; -import {ClosureAction, CollateralDebtData} from "../../../interfaces/ICreditManagerV3.sol"; +import {CollateralDebtData} from "../../../interfaces/ICreditManagerV3.sol"; import {TestHelper} from "../../lib/helper.sol"; import {GeneralMock} from "../../mocks/GeneralMock.sol"; @@ -318,7 +318,7 @@ contract CreditLogicTest is TestHelper { debt: 5000, accruedInterest: 2000, amountToPool: 8150, - remainingFunds: 1449, + remainingFunds: 1450, profit: 1150, loss: 0 }), @@ -360,7 +360,7 @@ contract CreditLogicTest is TestHelper { debt: 5000, accruedInterest: 2000, amountToPool: 8100, - remainingFunds: 1699, + remainingFunds: 1700, profit: 1100, loss: 0 }), @@ -402,7 +402,7 @@ contract CreditLogicTest is TestHelper { debt: 5000, accruedInterest: 2000, amountToPool: 8190, - remainingFunds: 1409, + remainingFunds: 1410, profit: 1150, loss: 0 }), diff --git a/contracts/test/unit/libraries/WithdrawalsLogic.t.sol b/contracts/test/unit/libraries/WithdrawalsLogic.t.sol deleted file mode 100644 index adf486df..00000000 --- a/contracts/test/unit/libraries/WithdrawalsLogic.t.sol +++ /dev/null @@ -1,308 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Foundation, 2023. -pragma solidity ^0.8.17; - -import {ClaimAction, ScheduledWithdrawal} from "../../../interfaces/IWithdrawalManagerV3.sol"; -import {WithdrawalsLogic} from "../../../libraries/WithdrawalsLogic.sol"; - -import {TestHelper} from "../../lib/helper.sol"; - -enum ScheduleTask { - IMMATURE, - MATURE, - NON_SCHEDULED -} - -/// @title Withdrawals logic library unit test -/// @notice U:[WL]: Unit tests for withdrawals logic library -contract WithdrawalsLogicUnitTest is TestHelper { - using WithdrawalsLogic for ClaimAction; - using WithdrawalsLogic for ScheduledWithdrawal; - using WithdrawalsLogic for ScheduledWithdrawal[2]; - - ScheduledWithdrawal[2] withdrawals; - - address constant TOKEN = address(0xdead); - uint8 constant TOKEN_INDEX = 1; - uint256 constant AMOUNT = 1 ether; - - /// @notice U:[WL-1]: `clear` works correctly - function test_U_WL_01_clear_works_correctly() public { - _setupWithdrawalSlot(0, ScheduleTask.MATURE); - withdrawals[0].clear(); - assertEq(withdrawals[0].maturity, 1); - assertEq(withdrawals[0].amount, 1); - } - - /// @notice U:[WL-2]: `tokenMaskAndAmount` works correctly - function test_U_WL_02_tokenMaskAndAmount_works_correctly() public { - // before scheduling - (address token, uint256 mask, uint256 amount) = withdrawals[0].tokenMaskAndAmount(); - assertEq(token, address(0)); - assertEq(mask, 0); - assertEq(amount, 0); - - // after scheduling - _setupWithdrawalSlot(0, ScheduleTask.MATURE); - (token, mask, amount) = withdrawals[0].tokenMaskAndAmount(); - assertEq(token, TOKEN); - assertEq(mask, 1 << TOKEN_INDEX); - assertEq(amount, AMOUNT - 1); - - // after clearing - _setupWithdrawalSlot(0, ScheduleTask.NON_SCHEDULED); - (token, mask, amount) = withdrawals[0].tokenMaskAndAmount(); - assertEq(token, address(0)); - assertEq(mask, 0); - assertEq(amount, 0); - } - - struct FindFreeSlotCase { - string name; - ScheduleTask task0; - ScheduleTask task1; - bool expectedFound; - uint8 expectedSlot; - } - - /// @notice U:[WL-3]: `findFreeSlot` works correctly - function test_U_WL_03_findFreeSlot_works_correctly() public { - FindFreeSlotCase[4] memory cases = [ - FindFreeSlotCase({ - name: "both slots non-scheduled", - task0: ScheduleTask.NON_SCHEDULED, - task1: ScheduleTask.NON_SCHEDULED, - expectedFound: true, - expectedSlot: 0 - }), - FindFreeSlotCase({ - name: "slot 0 non-scheduled, slot 1 scheduled", - task0: ScheduleTask.NON_SCHEDULED, - task1: ScheduleTask.MATURE, - expectedFound: true, - expectedSlot: 0 - }), - FindFreeSlotCase({ - name: "slot 0 scheduled, slot 1 non-scheduled", - task0: ScheduleTask.MATURE, - task1: ScheduleTask.NON_SCHEDULED, - expectedFound: true, - expectedSlot: 1 - }), - FindFreeSlotCase({ - name: "both slots scheduled", - task0: ScheduleTask.MATURE, - task1: ScheduleTask.MATURE, - expectedFound: false, - expectedSlot: 0 - }) - ]; - - for (uint256 i; i < cases.length; ++i) { - _setupWithdrawalSlot(0, cases[i].task0); - _setupWithdrawalSlot(1, cases[i].task1); - - (bool found, uint8 slot) = withdrawals.findFreeSlot(); - assertEq(found, cases[i].expectedFound, _testCaseErr(cases[i].name, "incorrect found value")); - if (found) { - assertEq(slot, cases[i].expectedSlot, _testCaseErr(cases[i].name, "incorrect slot")); - } - } - } - - struct ClaimOrCancelAllowedCase { - string name; - ClaimAction action; - ScheduleTask task; - bool expectedResult; - } - - /// @notice U:[WL-4]: `claimAllowed` works correctly - function test_U_WL_04_claimAllowed_works_correctly() public { - ClaimOrCancelAllowedCase[12] memory cases = [ - ClaimOrCancelAllowedCase({ - name: "immature withdrawal, action == CLAIM", - action: ClaimAction.CLAIM, - task: ScheduleTask.IMMATURE, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "immature withdrawal, action == CANCEL", - action: ClaimAction.CANCEL, - task: ScheduleTask.IMMATURE, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "immature withdrawal, action == FORCE_CLAIM", - action: ClaimAction.FORCE_CLAIM, - task: ScheduleTask.IMMATURE, - expectedResult: true - }), - ClaimOrCancelAllowedCase({ - name: "immature withdrawal, action == FORCE_CANCEL", - action: ClaimAction.FORCE_CANCEL, - task: ScheduleTask.IMMATURE, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "mature withdrawal, action == CLAIM", - action: ClaimAction.CLAIM, - task: ScheduleTask.MATURE, - expectedResult: true - }), - ClaimOrCancelAllowedCase({ - name: "mature withdrawal, action == CANCEL", - action: ClaimAction.CANCEL, - task: ScheduleTask.MATURE, - expectedResult: true - }), - ClaimOrCancelAllowedCase({ - name: "mature withdrawal, action == FORCE_CLAIM", - action: ClaimAction.FORCE_CLAIM, - task: ScheduleTask.MATURE, - expectedResult: true - }), - ClaimOrCancelAllowedCase({ - name: "mature withdrawal, action == FORCE_CANCEL", - action: ClaimAction.FORCE_CANCEL, - task: ScheduleTask.MATURE, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "non-scheduled withdrawal, action == CLAIM", - action: ClaimAction.CLAIM, - task: ScheduleTask.NON_SCHEDULED, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "non-scheduled withdrawal, action == CANCEL", - action: ClaimAction.CANCEL, - task: ScheduleTask.NON_SCHEDULED, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "non-scheduled withdrawal, action == FORCE_CLAIM", - action: ClaimAction.FORCE_CLAIM, - task: ScheduleTask.NON_SCHEDULED, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "non-scheduled withdrawal, action == FORCE_CANCEL", - action: ClaimAction.FORCE_CANCEL, - task: ScheduleTask.NON_SCHEDULED, - expectedResult: false - }) - ]; - - for (uint256 i; i < cases.length; ++i) { - _setupWithdrawalSlot(0, cases[i].task); - assertEq( - cases[i].action.claimAllowed(withdrawals[0].maturity), - cases[i].expectedResult, - _testCaseErr(cases[i].name, "incorrect result") - ); - } - } - - /// @notice U:[WL-5]: `cancelAllowed` works correctly - function test_U_WL_05_cancelAllowed_works_correctly() public { - ClaimOrCancelAllowedCase[12] memory cases = [ - ClaimOrCancelAllowedCase({ - name: "immature withdrawal, action == CLAIM", - action: ClaimAction.CLAIM, - task: ScheduleTask.IMMATURE, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "immature withdrawal, action == CANCEL", - action: ClaimAction.CANCEL, - task: ScheduleTask.IMMATURE, - expectedResult: true - }), - ClaimOrCancelAllowedCase({ - name: "immature withdrawal, action == FORCE_CLAIM", - action: ClaimAction.FORCE_CLAIM, - task: ScheduleTask.IMMATURE, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "immature withdrawal, action == FORCE_CANCEL", - action: ClaimAction.FORCE_CANCEL, - task: ScheduleTask.IMMATURE, - expectedResult: true - }), - ClaimOrCancelAllowedCase({ - name: "mature withdrawal, action == CLAIM", - action: ClaimAction.CLAIM, - task: ScheduleTask.MATURE, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "mature withdrawal, action == CANCEL", - action: ClaimAction.CANCEL, - task: ScheduleTask.MATURE, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "mature withdrawal, action == FORCE_CLAIM", - action: ClaimAction.FORCE_CLAIM, - task: ScheduleTask.MATURE, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "mature withdrawal, action == FORCE_CANCEL", - action: ClaimAction.FORCE_CANCEL, - task: ScheduleTask.MATURE, - expectedResult: true - }), - ClaimOrCancelAllowedCase({ - name: "non-scheduled withdrawal, action == CLAIM", - action: ClaimAction.CLAIM, - task: ScheduleTask.NON_SCHEDULED, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "non-scheduled withdrawal, action == CANCEL", - action: ClaimAction.CANCEL, - task: ScheduleTask.NON_SCHEDULED, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "non-scheduled withdrawal, action == FORCE_CLAIM", - action: ClaimAction.FORCE_CLAIM, - task: ScheduleTask.NON_SCHEDULED, - expectedResult: false - }), - ClaimOrCancelAllowedCase({ - name: "non-scheduled withdrawal, action == FORCE_CANCEL", - action: ClaimAction.FORCE_CANCEL, - task: ScheduleTask.NON_SCHEDULED, - expectedResult: false - }) - ]; - - for (uint256 i; i < cases.length; ++i) { - _setupWithdrawalSlot(0, cases[i].task); - assertEq( - cases[i].action.cancelAllowed(withdrawals[0].maturity), - cases[i].expectedResult, - _testCaseErr(cases[i].name, "incorrect result") - ); - } - } - - // ------- // - // HELPERS // - // ------- // - - function _setupWithdrawalSlot(uint8 slot, ScheduleTask task) internal { - if (task == ScheduleTask.NON_SCHEDULED) { - withdrawals[slot].clear(); - } else { - uint40 maturity = task == ScheduleTask.MATURE ? uint40(block.timestamp - 1) : uint40(block.timestamp + 1); - withdrawals[slot] = - ScheduledWithdrawal({maturity: maturity, amount: 1 ether, token: address(0xdead), tokenIndex: 1}); - } - } -} diff --git a/package.json b/package.json index 55d5e7a6..e58ce76c 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@gearbox-protocol/core-v2": "1.19.0-base.16", "@gearbox-protocol/prettier-config": "^1.5.0", "@gearbox-protocol/sdk-gov": "^1.4.6", - "@openzeppelin/contracts": "4.9.3", + "@openzeppelin/contracts": "^4.9.3", "husky": "^8.0.3", "lint-staged": "^13.0.3", "prettier": "^2.7.1", diff --git a/slither.config.json b/slither.config.json deleted file mode 100644 index 03d6a84f..00000000 --- a/slither.config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "filter_paths": "test/", - "foundry_out_directory": "forge-out/" -} diff --git a/yarn.lock b/yarn.lock index cc7e09d8..2edcdd66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1163,7 +1163,7 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.2.tgz#d815ade0027b50beb9bcca67143c6bcc3e3923d6" integrity sha512-kEUOgPQszC0fSYWpbh2kT94ltOJwj1qfT2DWo+zVttmGmf97JZ99LspePNaeeaLhCImaHVeBbjaQFZQn7+Zc5g== -"@openzeppelin/contracts@4.9.3": +"@openzeppelin/contracts@^4.9.3": version "4.9.3" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.3.tgz#00d7a8cf35a475b160b3f0293a6403c511099364" integrity sha512-He3LieZ1pP2TNt5JbkPA4PNT9WC3gOTOlDcFGJW4Le4QKqwmiNJCRt44APfxMxvq7OugU/cqYuPcSBzOw38DAg==