diff --git a/contracts/adapters/AbstractAdapter.sol b/contracts/adapters/AbstractAdapter.sol index 78ab1e56..e76a7347 100644 --- a/contracts/adapters/AbstractAdapter.sol +++ b/contracts/adapters/AbstractAdapter.sol @@ -3,75 +3,77 @@ // (c) Gearbox Holdings, 2023 pragma solidity ^0.8.17; -import {IAddressProvider} from "@gearbox-protocol/core-v2/contracts/interfaces/IAddressProvider.sol"; - import {IAdapter} from "../interfaces/IAdapter.sol"; import {ICreditManagerV3} from "../interfaces/ICreditManagerV3.sol"; import {CallerNotCreditFacadeException} from "../interfaces/IExceptions.sol"; -import {IPool4626} from "../interfaces/IPool4626.sol"; -import {ACLNonReentrantTrait} from "../traits/ACLNonReentrantTrait.sol"; +import {ACLTrait} from "../traits/ACLTrait.sol"; /// @title Abstract adapter /// @dev Inheriting adapters MUST use provided internal functions to perform all operations with credit accounts -abstract contract AbstractAdapter is IAdapter, ACLNonReentrantTrait { - /// @notice Credit manager the adapter is connected to - ICreditManagerV3 public immutable override creditManager; +abstract contract AbstractAdapter is IAdapter, ACLTrait { + /// @inheritdoc IAdapter + address public immutable override creditManager; - /// @notice Address provider - IAddressProvider public immutable override addressProvider; + /// @inheritdoc IAdapter + address public immutable override addressProvider; - /// @notice Address of the adapted contract + /// @inheritdoc IAdapter address public immutable override targetContract; /// @notice Constructor - /// @param _creditManager Credit manager to connect this adapter to + /// @param _creditManager Credit manager to connect the adapter to /// @param _targetContract Address of the adapted contract constructor(address _creditManager, address _targetContract) - ACLNonReentrantTrait(address(IPool4626(ICreditManagerV3(_creditManager).pool()).addressProvider())) // F: [AA-1] - nonZeroAddress(_targetContract) // F: [AA-1] + ACLTrait(ICreditManagerV3(_creditManager).addressProvider()) // U:[AA-1A] + nonZeroAddress(_targetContract) // U:[AA-1A] { - creditManager = ICreditManagerV3(_creditManager); // F: [AA-2] - addressProvider = IAddressProvider(IPool4626(creditManager.pool()).addressProvider()); // F: [AA-2] - targetContract = _targetContract; // F: [AA-2] + creditManager = _creditManager; // U:[AA-1B] + addressProvider = ICreditManagerV3(_creditManager).addressProvider(); // U:[AA-1B] + targetContract = _targetContract; // U:[AA-1B] } - /// @dev Ensures that function is called by the credit facade - /// @dev Inheriting adapters MUST use this modifier in all external functions that operate - /// on credit accounts to ensure they are called as part of the multicall + /// @dev Ensures that caller of the function is credit facade connected to the credit manager + /// @dev Inheriting adapters MUST use this modifier in all external functions that operate on credit accounts modifier creditFacadeOnly() { - if (msg.sender != creditManager.creditFacade()) { - revert CallerNotCreditFacadeException(); // F: [AA-3] - } + _revertIfCallerNotCreditFacade(); _; } - /// @dev Returns the credit account that will execute an external call to the target contract - /// @dev Inheriting adapters MUST use this function to find the address of the account they operate on + /// @dev Ensures that caller is credit facade connected to the credit manager + function _revertIfCallerNotCreditFacade() internal view { + if (msg.sender != ICreditManagerV3(creditManager).creditFacade()) { + revert CallerNotCreditFacadeException(); // U:[AA-2] + } + } + + /// @dev Ensures that external call credit account is set and returns its address function _creditAccount() internal view returns (address) { - return creditManager.externalCallCreditAccountOrRevert(); // F: [AA-4] + return ICreditManagerV3(creditManager).getActiveCreditAccountOrRevert(); // U:[AA-3] } - /// @dev Checks that token is registered as collateral in the credit manager and returns its mask + /// @dev Ensures that token is registered as collateral in the credit manager and returns its mask function _getMaskOrRevert(address token) internal view returns (uint256 tokenMask) { - tokenMask = creditManager.getTokenMaskOrRevert(token); // F: [AA-5] + tokenMask = ICreditManagerV3(creditManager).getTokenMaskOrRevert(token); // U:[AA-4] } /// @dev Approves target contract to spend given token from the credit account + /// Reverts if external call credit account is not set or token is not registered as collateral /// @param token Token to approve /// @param amount Amount to approve - /// @dev Reverts if token is not registered as collateral in the credit manager function _approveToken(address token, uint256 amount) internal { - creditManager.approveCreditAccount(token, amount); // F: [AA-6] + ICreditManagerV3(creditManager).approveCreditAccount(token, amount); // U:[AA-5] } /// @dev Executes an external call from the credit account to the target contract + /// Reverts if external call credit account is not set /// @param callData Data to call the target contract with /// @return result Call result function _execute(bytes memory callData) internal returns (bytes memory result) { - return creditManager.executeOrder(callData); // F: [AA-7] + return ICreditManagerV3(creditManager).executeOrder(callData); // U:[AA-6] } - /// @dev Executes a swap operation on the target contract without input token approval + /// @dev Executes a swap operation without input token approval + /// Reverts if external call credit account is not set or any of passed tokens is not registered as collateral /// @param tokenIn Input token that credit account spends in the call /// @param tokenOut Output token that credit account receives after the call /// @param callData Data to call the target contract with @@ -80,19 +82,18 @@ abstract contract AbstractAdapter is IAdapter, ACLNonReentrantTrait { /// @return tokensToEnable Bit mask of tokens that should be enabled after the call /// @return tokensToDisable Bit mask of tokens that should be disabled after the call /// @return result Call result - /// @dev Reverts if `tokenIn` or `tokenOut` are not registered as collateral in the credit manager function _executeSwapNoApprove(address tokenIn, address tokenOut, bytes memory callData, bool disableTokenIn) internal returns (uint256 tokensToEnable, uint256 tokensToDisable, bytes memory result) { - tokensToEnable = _getMaskOrRevert(tokenOut); // F: [AA-8, AA-10] - uint256 tokenInMask = _getMaskOrRevert(tokenIn); // F: [AA-10] - if (disableTokenIn) tokensToDisable = tokenInMask; // F: [AA-8] - result = _execute(callData); // F: [AA-8] + tokensToEnable = _getMaskOrRevert(tokenOut); // U:[AA-7] + uint256 tokenInMask = _getMaskOrRevert(tokenIn); + if (disableTokenIn) tokensToDisable = tokenInMask; // U:[AA-7] + result = _execute(callData); // U:[AA-7] } - /// @dev Executes a swap operation on the target contract with maximum input token approval, - /// and resets this approval to 1 after the call + /// @dev Executes a swap operation with maximum input token approval, and revokes approval after the call + /// Reverts if external call credit account is not set or any of passed tokens is not registered as collateral /// @param tokenIn Input token that credit account spends in the call /// @param tokenOut Output token that credit account receives after the call /// @param callData Data to call the target contract with @@ -101,15 +102,15 @@ abstract contract AbstractAdapter is IAdapter, ACLNonReentrantTrait { /// @return tokensToEnable Bit mask of tokens that should be enabled after the call /// @return tokensToDisable Bit mask of tokens that should be disabled after the call /// @return result Call result - /// @dev Reverts if `tokenIn` or `tokenOut` are not registered as collateral in the credit manager + /// @custom:expects Credit manager reverts when trying to approve non-collateral token function _executeSwapSafeApprove(address tokenIn, address tokenOut, bytes memory callData, bool disableTokenIn) internal returns (uint256 tokensToEnable, uint256 tokensToDisable, bytes memory result) { - tokensToEnable = _getMaskOrRevert(tokenOut); // F: [AA-9, AA-10] - if (disableTokenIn) tokensToDisable = _getMaskOrRevert(tokenIn); // F: [AA-9, AA-10] - _approveToken(tokenIn, type(uint256).max); // F: [AA-9, AA-10] - result = _execute(callData); // F: [AA-9] - _approveToken(tokenIn, 1); // F: [AA-9] + tokensToEnable = _getMaskOrRevert(tokenOut); // U:[AA-8] + if (disableTokenIn) tokensToDisable = _getMaskOrRevert(tokenIn); // U:[AA-8] + _approveToken(tokenIn, type(uint256).max); // U:[AA-8] + result = _execute(callData); // U:[AA-8] + _approveToken(tokenIn, 1); // U:[AA-8] } } diff --git a/contracts/core/AccountFactoryV3.sol b/contracts/core/AccountFactoryV3.sol index 161596e5..ccf087fe 100644 --- a/contracts/core/AccountFactoryV3.sol +++ b/contracts/core/AccountFactoryV3.sol @@ -1,97 +1,145 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 +// (c) Gearbox Holdings, 2023 pragma solidity ^0.8.17; pragma abicoder v1; -import {ContractsRegisterTrait} from "../traits/ContractsRegisterTrait.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; + import {CreditAccountV3} from "../credit/CreditAccountV3.sol"; +import {CreditManagerV3} from "../credit/CreditManagerV3.sol"; +import {IAccountFactoryV3} from "../interfaces/IAccountFactoryV3.sol"; +import { + CallerNotCreditManagerException, + CreditAccountIsInUseException, + MasterCreditAccountAlreadyDeployedException +} from "../interfaces/IExceptions.sol"; import {ACLTrait} from "../traits/ACLTrait.sol"; +import {ContractsRegisterTrait} from "../traits/ContractsRegisterTrait.sol"; -import {IAccountFactory, TakeAccountAction} from "../interfaces/IAccountFactory.sol"; - -// EXCEPTIONS -import "../interfaces/IExceptions.sol"; - -import "forge-std/console.sol"; - -struct CreditManagerFactory { +/// @dev Struct storing per-CreditManager data on account usage queue +struct FactoryParams { + /// @dev Address of the contract being cloned to create new Credit Accounts address masterCreditAccount; - uint32 head; - uint32 tail; - uint16 minUsedInQueue; + /// @dev Id of the next reused Credit Account in the used account queue, i.e. + /// the front of the reused CA queue + uint40 head; + /// @dev Id of the last returned Credit Account in the used account queue, i.e. + /// the back of the reused CA queue + uint40 tail; +} + +/// @dev Struct compressing the credit account address and the +/// timestamp at which it is reusable +struct QueuedAccount { + address creditAccount; + uint40 reusableAfter; } -/// @title Disposable credit accounts factory -contract AccountFactoryV3 is IAccountFactory, ACLTrait, ContractsRegisterTrait { - /// @dev Address of master credit account for cloning - mapping(address => CreditManagerFactory) public masterCreditAccounts; +/// @title Account factory V3 +/// @notice Reusable credit accounts factory. +/// - Account deployment is cheap thanks to the clones proxy pattern +/// - Accounts are reusable: new accounts are only deployed when the queue of reusable accounts is empty +/// (a separate queue is maintained for each credit manager) +/// - When account is returned to the factory, it is only added to the queue after a certain delay, which +/// allows DAO to rescue funds that might have been accidentally left upon account closure +contract AccountFactoryV3 is IAccountFactoryV3, ACLTrait, ContractsRegisterTrait { + /// @inheritdoc IVersion + uint256 public constant override version = 3_00; - mapping(address => address[]) public usedCreditAccounts; + /// @inheritdoc IAccountFactoryV3 + uint40 public constant override delay = 3 days; - /// @dev Contract version - uint256 public constant version = 3_00; + /// @dev Mapping credit manager => factory params + mapping(address => FactoryParams) internal _factoryParams; - error MasterCreditAccountAlreadyDeployed(); + /// @dev Mapping credit manager => queued accounts + mapping(address => QueuedAccount[]) internal _queuedAccounts; - /// @param addressProvider Address of address repository + /// @notice Constructor + /// @param addressProvider Address provider contract address constructor(address addressProvider) ACLTrait(addressProvider) ContractsRegisterTrait(addressProvider) {} - /// @dev Provides a new credit account to a Credit Manager - /// @return creditAccount Address of credit account - function takeCreditAccount(uint256 deployAction, uint256) external override returns (address creditAccount) { - CreditManagerFactory storage cmf = masterCreditAccounts[msg.sender]; - address masterCreditAccount = cmf.masterCreditAccount; + /// @inheritdoc IAccountFactoryV3 + function takeCreditAccount(uint256, uint256) external override returns (address creditAccount) { + FactoryParams storage fp = _factoryParams[msg.sender]; + address masterCreditAccount = fp.masterCreditAccount; if (masterCreditAccount == address(0)) { - revert CallerNotCreditManagerException(); + revert CallerNotCreditManagerException(); // U:[AF-1] } - uint256 totalUsed = cmf.tail - cmf.head; - if (totalUsed < cmf.minUsedInQueue || deployAction == uint256(TakeAccountAction.DEPLOY_NEW_ONE)) { - // Create a new credit account if there are none in stock - creditAccount = Clones.clone(masterCreditAccount); // T:[AF-2] - emit DeployCreditAccount(creditAccount); + + /// A used Credit Account is only given to a user if sufficiently long + /// time has passed since it was last taken out. + /// This is done to prevent a user from intentionally reopening an account + /// that they closed shortly prior, as this can potentially be used as an element + /// in an attack. + uint256 head = fp.head; + if (head == fp.tail || block.timestamp < _queuedAccounts[msg.sender][head].reusableAfter) { + creditAccount = Clones.clone(masterCreditAccount); // U:[AF-2A] + emit DeployCreditAccount({creditAccount: creditAccount, creditManager: msg.sender}); // U:[AF-2A] } else { - creditAccount = usedCreditAccounts[msg.sender][cmf.head]; - ++cmf.head; - emit ReuseCreditAccount(creditAccount); + creditAccount = _queuedAccounts[msg.sender][head].creditAccount; // U:[AF-2B] + delete _queuedAccounts[msg.sender][head]; // U:[AF-2B] + unchecked { + ++fp.head; // U:[AF-2B] + } } - // emit InitializeCreditAccount(result, msg.sender); // T:[AF-5] + emit TakeCreditAccount({creditAccount: creditAccount, creditManager: msg.sender}); // U:[AF-2A,2B] } - function returnCreditAccount(address usedAccount) external override { - CreditManagerFactory storage cmf = masterCreditAccounts[msg.sender]; + /// @inheritdoc IAccountFactoryV3 + function returnCreditAccount(address creditAccount) external override { + FactoryParams storage fp = _factoryParams[msg.sender]; - if (cmf.masterCreditAccount == address(0)) { - revert CallerNotCreditManagerException(); + if (fp.masterCreditAccount == address(0)) { + revert CallerNotCreditManagerException(); // U:[AF-1] } - usedCreditAccounts[msg.sender][cmf.tail] = usedAccount; - ++cmf.tail; - emit ReturnCreditAccount(usedAccount); + _queuedAccounts[msg.sender].push( + QueuedAccount({creditAccount: creditAccount, reusableAfter: uint40(block.timestamp) + delay}) + ); // U:[AF-3] + unchecked { + ++fp.tail; // U:[AF-3] + } + emit ReturnCreditAccount({creditAccount: creditAccount, creditManager: msg.sender}); // U:[AF-3] } - // CONFIGURATION + /// ------------- /// + /// CONFIGURATION /// + /// ------------- /// - function addCreditManager(address creditManager, uint16 minUsedInQueue) + /// @inheritdoc IAccountFactoryV3 + function addCreditManager(address creditManager) external - configuratorOnly - registeredCreditManagerOnly(creditManager) + override + configuratorOnly // U:[AF-1] + registeredCreditManagerOnly(creditManager) // U:[AF-4A] { - if (masterCreditAccounts[creditManager].masterCreditAccount != address(0)) { - revert MasterCreditAccountAlreadyDeployed(); + if (_factoryParams[creditManager].masterCreditAccount != address(0)) { + revert MasterCreditAccountAlreadyDeployedException(); // U:[AF-4B] } + address masterCreditAccount = address(new CreditAccountV3(creditManager)); // U:[AF-4C] + _factoryParams[creditManager].masterCreditAccount = masterCreditAccount; // U:[AF-4C] + emit AddCreditManager(creditManager, masterCreditAccount); // U:[AF-4C] + } - masterCreditAccounts[creditManager] = CreditManagerFactory({ - masterCreditAccount: address(new CreditAccountV3(creditManager)), - head: 0, - tail: 0, - minUsedInQueue: minUsedInQueue - }); + /// @inheritdoc IAccountFactoryV3 + function rescue(address creditAccount, address target, bytes calldata data) + external + configuratorOnly // U:[AF-1] + { + address creditManager = CreditAccountV3(creditAccount).creditManager(); + _checkRegisteredCreditManagerOnly(creditManager); // U:[AF-5A] + + (,,,,, address borrower) = CreditManagerV3(creditManager).creditAccountInfo(creditAccount); + if (borrower != address(0)) { + revert CreditAccountIsInUseException(); // U:[AF-5B] + } - emit AddCreditManager(creditManager); + CreditAccountV3(creditAccount).rescue(target, data); // U:[AF-5C] } } diff --git a/contracts/core/AddressProviderV3.sol b/contracts/core/AddressProviderV3.sol new file mode 100644 index 00000000..c1a68e52 --- /dev/null +++ b/contracts/core/AddressProviderV3.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +import "../interfaces/IAddressProviderV3.sol"; +import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; +import {IACL} from "@gearbox-protocol/core-v2/contracts/interfaces/IACL.sol"; + +import {AddressNotFoundException, CallerNotConfiguratorException} from "../interfaces/IExceptions.sol"; + +/// @title AddressRepository +/// @notice Stores addresses of deployed contracts +contract AddressProviderV3 is IAddressProviderV3 { + /// @notice Mapping from (contract key, version) to the respective contract address + mapping(bytes32 => mapping(uint256 => address)) public addresses; + + /// @inheritdoc IVersion + uint256 public constant override(IVersion) version = 3_00; + + modifier configuratorOnly() { + if (!IACL(getAddressOrRevert(AP_ACL, NO_VERSION_CONTROL)).isConfigurator(msg.sender)) { + revert CallerNotConfiguratorException(); + } + _; + } + + constructor(address _acl) { + /// The first event is emitted for the AP itself, to aid in contract discovery + emit AddressSet("ADDRESS_PROVIDER", address(this), version); + _setAddress(AP_ACL, _acl, NO_VERSION_CONTROL); + } + + /// @notice Returns the address associated with the passed key/version + function getAddressOrRevert(bytes32 key, uint256 _version) public view virtual override returns (address result) { + result = addresses[key][_version]; + if (result == address(0)) revert AddressNotFoundException(); + } + + /// @notice Sets the address for the passed key, and optionally records the contract version + /// @param key Key in string format + /// @param value Address to save + /// @param saveVersion Whether to save + function setAddress(bytes32 key, address value, bool saveVersion) external override configuratorOnly { + _setAddress(key, value, saveVersion ? IVersion(value).version() : NO_VERSION_CONTROL); + } + + /// @notice Internal function to set the address mapping value + function _setAddress(bytes32 key, address value, uint256 _version) internal virtual { + addresses[key][_version] = value; + emit AddressSet(key, value, _version); // F:[AP-2] + } + + /// KEPT FOR BACKWARD COMPATABILITY + + /// @return Address of ACL contract + function getACL() external view returns (address) { + return getAddressOrRevert(AP_ACL, NO_VERSION_CONTROL); // F:[AP-3] + } + + /// @return Address of ContractsRegister + function getContractsRegister() external view returns (address) { + return getAddressOrRevert(AP_CONTRACTS_REGISTER, 1); // F:[AP-4] + } + + /// @return Address of PriceOracle + function getPriceOracle() external view returns (address) { + return getAddressOrRevert(AP_PRICE_ORACLE, 2); // F:[AP-5] + } + + /// @return Address of AccountFactory + function getAccountFactory() external view returns (address) { + return getAddressOrRevert(AP_ACCOUNT_FACTORY, NO_VERSION_CONTROL); // F:[AP-6] + } + + /// @return Address of DataCompressor + function getDataCompressor() external view returns (address) { + return getAddressOrRevert(AP_DATA_COMPRESSOR, 2); // F:[AP-7] + } + + /// @return Address of Treasury contract + function getTreasuryContract() external view returns (address) { + return getAddressOrRevert(AP_TREASURY, NO_VERSION_CONTROL); // F:[AP-8] + } + + /// @return Address of GEAR token + function getGearToken() external view returns (address) { + return getAddressOrRevert(AP_GEAR_TOKEN, NO_VERSION_CONTROL); // F:[AP-9] + } + + /// @return Address of WETH token + function getWethToken() external view returns (address) { + return getAddressOrRevert(AP_WETH_TOKEN, NO_VERSION_CONTROL); // F:[AP-10] + } + + /// @return Address of WETH token + function getWETHGateway() external view returns (address) { + return getAddressOrRevert(AP_WETH_GATEWAY, 1); // F:[AP-11] + } + + /// @return Address of Router + function getLeveragedActions() external view returns (address) { + return getAddressOrRevert(AP_ROUTER, 1); // T:[AP-7] + } +} diff --git a/contracts/credit/CreditAccountV3.sol b/contracts/credit/CreditAccountV3.sol index 2c0ace1e..787bf541 100644 --- a/contracts/credit/CreditAccountV3.sol +++ b/contracts/credit/CreditAccountV3.sol @@ -1,29 +1,41 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +// (c) Gearbox Holdings, 2023 +pragma solidity ^0.8.17; pragma abicoder v1; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {ICreditAccount} from "../interfaces/ICreditAccount.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; -import "../interfaces/IExceptions.sol"; +import {ICreditAccountV3} from "../interfaces/ICreditAccountV3.sol"; +import {CallerNotAccountFactoryException, CallerNotCreditManagerException} from "../interfaces/IExceptions.sol"; -/// @title Credit AccountV3 -contract CreditAccountV3 is ICreditAccount { +/// @title Credit account V3 +contract CreditAccountV3 is ICreditAccountV3 { using SafeERC20 for IERC20; using Address for address; - /// @dev Address of the currently connected Credit Manager - address public immutable creditManager; + /// @inheritdoc IVersion + uint256 public constant override version = 3_00; - // Contract version - uint256 public constant version = 3_00; + /// @inheritdoc ICreditAccountV3 + address public immutable override factory; - /// @dev Restricts operations to the connected Credit Manager only + /// @inheritdoc ICreditAccountV3 + address public immutable override creditManager; + + /// @dev Ensures that function caller is account factory + modifier factoryOnly() { + if (msg.sender != factory) { + revert CallerNotAccountFactoryException(); + } + _; + } + + /// @dev Ensures that function caller is credit manager modifier creditManagerOnly() { if (msg.sender != creditManager) { revert CallerNotCreditManagerException(); @@ -31,25 +43,38 @@ contract CreditAccountV3 is ICreditAccount { _; } + /// @notice Constructor + /// @param _creditManager Credit manager to connect this account to constructor(address _creditManager) { - creditManager = _creditManager; + creditManager = _creditManager; // U:[CA-1] + factory = msg.sender; // U:[CA-1] } - /// @dev Transfers tokens from the credit account to a provided address. Restricted to the current Credit Manager only. - /// @param token Token to be transferred from the Credit Account. - /// @param to Address of the recipient. - /// @param amount Amount to be transferred. + /// @inheritdoc ICreditAccountV3 function safeTransfer(address token, address to, uint256 amount) external - creditManagerOnly // T:[CA-2] + override + creditManagerOnly // U:[CA-2] { - IERC20(token).safeTransfer(to, amount); // T:[CA-6] + IERC20(token).safeTransfer(to, amount); // U:[CA-3] } - /// @dev Executes a call to a 3rd party contract with provided data. Restricted to the current Credit Manager only. - /// @param destination Contract address to be called. - /// @param data Data to call the contract with. - function execute(address destination, bytes memory data) external creditManagerOnly returns (bytes memory) { - return destination.functionCall(data); // T: [CM-48] + /// @inheritdoc ICreditAccountV3 + function execute(address target, bytes memory data) + external + override + creditManagerOnly // U:[CA-2] + returns (bytes memory result) + { + result = target.functionCall(data); // U:[CA-4] + } + + /// @inheritdoc ICreditAccountV3 + function rescue(address target, bytes memory data) + external + override + factoryOnly // U:[CA-2] + { + target.functionCall(data); // U:[CA-5] } } diff --git a/contracts/credit/CreditConfiguratorV3.sol b/contracts/credit/CreditConfiguratorV3.sol index 8c955eff..77726da7 100644 --- a/contracts/credit/CreditConfiguratorV3.sol +++ b/contracts/credit/CreditConfiguratorV3.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; +import "../interfaces/IAddressProviderV3.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; @@ -17,7 +18,7 @@ import { DEFAULT_LIMIT_PER_BLOCK_MULTIPLIER, WAD } from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; - +import {UNDERLYING_TOKEN_MASK} from "../libraries/BitMask.sol"; import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; // CONTRACTS @@ -34,7 +35,6 @@ import { } from "../interfaces/ICreditConfiguratorV3.sol"; import {IPriceOracleV2} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; import {IPoolService} from "@gearbox-protocol/core-v2/contracts/interfaces/IPoolService.sol"; -import {IAddressProvider} from "@gearbox-protocol/core-v2/contracts/interfaces/IAddressProvider.sol"; import {IPoolQuotaKeeper} from "../interfaces/IPoolQuotaKeeper.sol"; // EXCEPTIONS @@ -44,30 +44,36 @@ import {ICreditManagerV3} from "../interfaces/ICreditManagerV3.sol"; /// @title CreditConfigurator /// @notice This contract is used to configure CreditManagers and is the only one with the priviledge /// to call access-restricted functions -/// @dev All functions can only by called by he Configurator as per ACL. -/// CreditManagerV3 blindly executes all requests from CreditConfigurator, so all sanity checks +/// @dev All functions can only by called by the Configurator as per ACL. +/// CreditManagerV3 blindly executes all requests (in nearly all cases) from CreditConfigurator, so most sanity checks /// are performed here. contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { using EnumerableSet for EnumerableSet.AddressSet; using Address for address; - /// @dev Address provider (needed for upgrading the Price Oracle) - IAddressProvider public override addressProvider; + /// @notice Address provider (needed for upgrading the Price Oracle) + address public immutable override addressProvider; - /// @dev Address of the Credit Manager + /// @notice Address of the Credit Manager CreditManagerV3 public override creditManager; - /// @dev Address of the Credit Manager's underlying asset + /// @notice Address of the Credit Manager's underlying asset address public override underlying; - /// @dev Array of the allowed contracts + /// @notice Set of allowed contracts EnumerableSet.AddressSet private allowedContractsSet; - /// @dev Contract version + /// @notice Set of emergency liquidators + EnumerableSet.AddressSet private emergencyLiquidatorsSet; + + /// @notice Set of forbidden tokens + EnumerableSet.AddressSet private forbiddenTokensSet; + + /// @notice Contract version uint256 public constant version = 3_00; - /// @dev Constructor has a special role in credit management deployment - /// This is where the initial configuration is performed. + /// @notice Constructor has a special role in Credit Manager deployment + /// For newly deployed CMs, this is where the initial configuration is performed. /// The correct deployment flow is as follows: /// /// 1. Configures CreditManagerV3 fee parameters and sets underlying LT @@ -75,69 +81,107 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { /// 3. Connects creditFacade and priceOracle to the Credit Manager /// 4. Sets itself as creditConfigurator in Credit Manager /// + /// For existing Credit Manager the CC will only migrate some parameters from the previous Credit Configurator, + /// and will otherwise keep the existing CM configuration intact. /// @param _creditManager CreditManagerV3 contract instance /// @param _creditFacade CreditFacadeV3 contract instance /// @param opts Configuration parameters for CreditManagerV3 constructor(CreditManagerV3 _creditManager, CreditFacadeV3 _creditFacade, CreditManagerOpts memory opts) ACLNonReentrantTrait(address(IPoolService(_creditManager.poolService()).addressProvider())) { - /// Sets contract addressees - creditManager = _creditManager; // F:[CC-1] - underlying = creditManager.underlying(); // F:[CC-1] + creditManager = _creditManager; // I:[CC-1] + underlying = creditManager.underlying(); // I:[CC-1] - addressProvider = IPoolService(_creditManager.poolService()).addressProvider(); // F:[CC-1] + addressProvider = _creditManager.addressProvider(); // I:[CC-1] - address currentConfigurator = creditManager.creditConfigurator(); // F: [CC-41] + address currentConfigurator = creditManager.creditConfigurator(); // I:[CC-41] if (currentConfigurator != address(this)) { /// DEPLOYED FOR EXISTING CREDIT MANAGER + /// In the case where the CC is deployed for the existing Credit Manager, + /// we only need to copy several array parameters from the last CC, + /// but the existing configs must be kept intact otherwise + /// 1. Allowed contracts set stores all the connected third-party contracts - currently only used + /// to retrieve externally + /// 2. Emergency liquidator set stores all emergency liquidators - used for parameter migration when changing the Credit Facade + /// 3. Forbidden token set stores all forbidden tokens - used for parameter migration when changing the Credit Facade + { + address[] memory allowedContractsPrev = CreditConfigurator(currentConfigurator).allowedContracts(); // I:[CC-41] + + uint256 allowedContractsLen = allowedContractsPrev.length; + for (uint256 i = 0; i < allowedContractsLen;) { + allowedContractsSet.add(allowedContractsPrev[i]); // I:[CC-41] + + unchecked { + ++i; + } + } + } + { + address[] memory emergencyLiquidatorsPrev = + CreditConfigurator(currentConfigurator).emergencyLiquidators(); - address[] memory allowedContractsPrev = CreditConfigurator(currentConfigurator).allowedContracts(); // F: [CC-41] + uint256 emergencyLiquidatorsLen = emergencyLiquidatorsPrev.length; + for (uint256 i = 0; i < emergencyLiquidatorsLen;) { + emergencyLiquidatorsSet.add(emergencyLiquidatorsPrev[i]); - uint256 allowedContractsLen = allowedContractsPrev.length; - for (uint256 i = 0; i < allowedContractsLen;) { - allowedContractsSet.add(allowedContractsPrev[i]); // F: [CC-41] + unchecked { + ++i; + } + } + } + { + address[] memory forbiddenTokensPrev = CreditConfigurator(currentConfigurator).forbiddenTokens(); - unchecked { - ++i; + uint256 forbiddenTokensLen = forbiddenTokensPrev.length; + for (uint256 i = 0; i < forbiddenTokensLen;) { + forbiddenTokensSet.add(forbiddenTokensPrev[i]); + + unchecked { + ++i; + } } } } else { /// DEPLOYED FOR NEW CREDIT MANAGER - /// Sets limits and fees for the Credit Manager - _setParams( + /// Sets liquidation discounts and fees for the Credit Manager + _setFees( DEFAULT_FEE_INTEREST, DEFAULT_FEE_LIQUIDATION, PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM, DEFAULT_FEE_LIQUIDATION_EXPIRED, PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM_EXPIRED - ); // F:[CC-1] + ); // I:[CC-1] /// Adds collateral tokens and sets their liquidation thresholds - /// The underlying must not be in this list, since its LT is set separately in _setParams + /// The underlying must not be in this list, since its LT is set separately in _setFees uint256 len = opts.collateralTokens.length; for (uint256 i = 0; i < len;) { address token = opts.collateralTokens[i].token; - addCollateralToken(token); // F:[CC-1] + _addCollateralToken(token); // I:[CC-1] - _setLiquidationThreshold(token, opts.collateralTokens[i].liquidationThreshold); // F:[CC-1] + _setLiquidationThreshold(token, opts.collateralTokens[i].liquidationThreshold); // I:[CC-1] unchecked { ++i; } } - // Connects creditFacade and priceOracle - creditManager.setCreditFacade(address(_creditFacade)); // F:[CC-1] + /// Connects creditFacade and priceOracle + creditManager.setCreditFacade(address(_creditFacade)); // I:[CC-1] - emit SetCreditFacade(address(_creditFacade)); // F: [CC-1A] - emit SetPriceOracle(address(creditManager.priceOracle())); // F: [CC-1A] + emit SetCreditFacade(address(_creditFacade)); // I:[CC-1A] + emit SetPriceOracle(address(creditManager.priceOracle())); // I:[CC-1A] - _setMaxDebtPerBlockMultiplier(uint8(DEFAULT_LIMIT_PER_BLOCK_MULTIPLIER)); // F:[CC-1] + /// Sets the max debt per block multiplier + /// This parameter determines the maximal new debt per block as a factor of + /// maximal Credit Account debt - essentially a cap on the number of new Credit Accounts per block + _setMaxDebtPerBlockMultiplier(uint8(DEFAULT_LIMIT_PER_BLOCK_MULTIPLIER)); // I:[CC-1] - _setLimits(opts.minBorrowedAmount, opts.maxBorrowedAmount); // F:[CC-1] + /// Sets the borrowing limits per Credit Account + _setLimits(opts.minBorrowedAmount, opts.maxBorrowedAmount); // I:[CC-1] } } @@ -145,162 +189,176 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { // CONFIGURATION: TOKEN MANAGEMENT // - /// @dev Adds token to the list of allowed collateral tokens, and sets the LT + /// @notice Adds token to the list of allowed collateral tokens, and sets the LT /// @param token Address of token to be added /// @param liquidationThreshold Liquidation threshold for account health calculations function addCollateralToken(address token, uint16 liquidationThreshold) external override - configuratorOnly // F:[CC-2] + configuratorOnly // I:[CC-2] { - addCollateralToken(token); // F:[CC-3,4] - _setLiquidationThreshold(token, liquidationThreshold); // F:[CC-4] + _addCollateralToken(token); // I:[CC-3,4] + _setLiquidationThreshold(token, liquidationThreshold); // I:[CC-4] } - /// @dev Makes all sanity checks and adds the token to the collateral token list + /// @notice Makes all sanity checks and adds the token to the collateral token list /// @param token Address of token to be added - function addCollateralToken(address token) internal nonZeroAddress(token) { - // Checks that token != address(0) - - if (!token.isContract()) revert AddressIsNotContractException(token); // F:[CC-3] + function _addCollateralToken(address token) internal nonZeroAddress(token) { + /// Checks that the token is a contract + if (!token.isContract()) revert AddressIsNotContractException(token); // I:[CC-3] // Checks that the contract has balanceOf method try IERC20(token).balanceOf(address(this)) returns (uint256) {} catch { - revert IncorrectTokenContractException(); // F:[CC-3] + revert IncorrectTokenContractException(); // I:[CC-3] } // Checks that the token has a correct priceFeed in priceOracle try IPriceOracleV2(creditManager.priceOracle()).convertToUSD(WAD, token) returns (uint256) {} catch { - revert IncorrectPriceFeedException(); // F:[CC-3] + revert IncorrectPriceFeedException(); // I:[CC-3] } - // creditManager has an additional check that the token is not added yet - creditManager.addToken(token); // F:[CC-4] + /// creditManager has an additional check that the token is not added yet + creditManager.addToken(token); // I:[CC-4] - emit AllowToken(token); // F:[CC-4] + emit AllowToken(token); // I:[CC-4] } - /// @dev Sets a liquidation threshold for any token except the underlying + /// @notice Sets a liquidation threshold for any token except the underlying /// @param token Token address /// @param liquidationThreshold in PERCENTAGE_FORMAT (100% = 10000) function setLiquidationThreshold(address token, uint16 liquidationThreshold) external - controllerOnly // F:[CC-2B] + controllerOnly // I:[CC-2B] { - _setLiquidationThreshold(token, liquidationThreshold); // F:[CC-5] + _setLiquidationThreshold(token, liquidationThreshold); // I:[CC-5] } - /// @dev IMPLEMENTAION: setLiquidationThreshold + /// @notice IMPLEMENTAION: setLiquidationThreshold function _setLiquidationThreshold(address token, uint16 liquidationThreshold) internal { // Checks that the token is not underlying, since its LT is determined by Credit Manager params - if (token == underlying) revert SetLTForUnderlyingException(); // F:[CC-5] + if (token == underlying) revert SetLTForUnderlyingException(); // I:[CC-5] - (, uint16 ltUnderlying) = creditManager.collateralTokens(0); + (, uint16 ltUnderlying) = creditManager.collateralTokensByMask(UNDERLYING_TOKEN_MASK); // Sanity check for the liquidation threshold. The LT should be less than underlying if (liquidationThreshold > ltUnderlying) { revert IncorrectLiquidationThresholdException(); - } // F:[CC-5] + } // I:[CC-5] uint16 currentLT = creditManager.liquidationThresholds(token); if (currentLT != liquidationThreshold) { - // Sets the LT in Credit Manager, where token existence is checked - // _setLTRampParams(tokenData, tokenMask, , 0); - creditManager.setCollateralTokenData(token, liquidationThreshold, liquidationThreshold, type(uint40).max, 0); // F:[CC-6] - emit SetTokenLiquidationThreshold(token, liquidationThreshold); // F:[CC-6] + /// When the LT of a token is set directly, we set the parameters + /// as if it was a ramp from `liquidationThreshold` to `liquidationThreshold` + /// starting in far future. This ensures that the LT function in Credit Manager + /// will always return `liquidationThreshold` until the parameters are changed + creditManager.setCollateralTokenData(token, liquidationThreshold, liquidationThreshold, type(uint40).max, 0); // I:[CC-6] + emit SetTokenLiquidationThreshold(token, liquidationThreshold); // I:[CC-6] } } - /// @dev Schedules an LT ramping for any token except underlying + /// @notice Schedules an LT ramping for any token except underlying /// @param token Token to ramp LT for /// @param liquidationThresholdFinal Liquidation threshold after ramping /// @param rampDuration Duration of ramping - function rampLiquidationThreshold(address token, uint16 liquidationThresholdFinal, uint24 rampDuration) - external - controllerOnly - { + function rampLiquidationThreshold( + address token, + uint16 liquidationThresholdFinal, + uint40 rampStart, + uint24 rampDuration + ) external controllerOnly { // Checks that the token is not underlying, since its LT is determined by Credit Manager params if (token == underlying) revert SetLTForUnderlyingException(); - (, uint16 ltUnderlying) = creditManager.collateralTokens(0); + (, uint16 ltUnderlying) = creditManager.collateralTokensByMask(UNDERLYING_TOKEN_MASK); // Sanity check for the liquidation threshold. The LT should be less than underlying if (liquidationThresholdFinal > ltUnderlying) { revert IncorrectLiquidationThresholdException(); } + // In case that (for some reason) the function is executed later than + // the start of the ramp, we start the ramp from the current moment + // to prevent discontinueous jumps in token's LT + rampStart = block.timestamp > rampStart ? uint40(block.timestamp) : rampStart; + uint16 currentLT = creditManager.liquidationThresholds(token); if (currentLT != liquidationThresholdFinal) { - // Sets the LT in Credit Manager, where token existence is checked - creditManager.setCollateralTokenData( - token, currentLT, liquidationThresholdFinal, uint40(block.timestamp), rampDuration - ); + // CollateralTokenData in CreditManager stores 4 values: + // 1. ltInitial + // 2. ltFinal + // 3. rampStart + // 4. rampDuration + // The actual LT changes linearly between ltInitial and ltFinal over rampDuration; + // E.g., it is ltInitial in rampStart and ltFinal in rampStart + rampDuration + creditManager.setCollateralTokenData(token, currentLT, liquidationThresholdFinal, rampStart, rampDuration); emit ScheduleTokenLiquidationThresholdRamp( - token, - currentLT, - liquidationThresholdFinal, - uint40(block.timestamp), - uint40(block.timestamp) + rampDuration + token, currentLT, liquidationThresholdFinal, rampStart, uint40(block.timestamp) + rampDuration ); } } - /// @dev Allow a known collateral token if it was forbidden before. + /// @notice Allow a known collateral token if it was forbidden before. /// @param token Address of collateral token function allowToken(address token) external - configuratorOnly // F:[CC-2] + configuratorOnly // I:[CC-2] { // Gets token masks. Reverts if the token was not added as collateral or is the underlying - uint256 tokenMask = _getAndCheckTokenMaskForSettingLT(token); // F:[CC-7] + uint256 tokenMask = _getAndCheckTokenMaskForSettingLT(token); // I:[CC-7] // Gets current forbidden mask - uint256 forbiddenTokenMask = creditFacade().forbiddenTokenMask(); // F:[CC-8,9] + uint256 forbiddenTokenMask = creditFacade().forbiddenTokenMask(); // I:[CC-8,9] // If the token was forbidden before, flips the corresponding bit in the mask, // otherwise no actions done. - // Skipping case: F:[CC-8] + // Skipping case: I:[CC-8] if (forbiddenTokenMask & tokenMask != 0) { creditFacade().setTokenAllowance(token, AllowanceAction.ALLOW); // TODO: CHECK - emit AllowToken(token); // F:[CC-9] + forbiddenTokensSet.remove(token); + emit AllowToken(token); // I:[CC-9] } } - /// @dev Forbids a collateral token. + /// @notice Forbids a collateral token. /// Forbidden tokens are counted as collateral during health checks, however, they cannot be enabled /// or received as a result of adapter operation anymore. This means that a token can never be /// acquired through adapter operations after being forbidden. /// @param token Address of collateral token to forbid function forbidToken(address token) external - pausableAdminsOnly // F:[CC-2B] + pausableAdminsOnly // I:[CC-2B] { + _forbidToken(token); + } + + /// @notice IMPLEMENTATION: forbidToken + function _forbidToken(address token) internal { // Gets token masks. Reverts if the token was not added as collateral or is the underlying - uint256 tokenMask = _getAndCheckTokenMaskForSettingLT(token); // F:[CC-7] + uint256 tokenMask = _getAndCheckTokenMaskForSettingLT(token); // I:[CC-7] // Gets current forbidden mask uint256 forbiddenTokenMask = creditFacade().forbiddenTokenMask(); // If the token was not forbidden before, flips the corresponding bit in the mask, // otherwise no actions done. - // Skipping case: F:[CC-10] + // Skipping case: I:[CC-10] if (forbiddenTokenMask & tokenMask == 0) { - forbiddenTokenMask |= tokenMask; // F:[CC-11] creditFacade().setTokenAllowance(token, AllowanceAction.FORBID); // TODO: CHECK - emit ForbidToken(token); // F:[CC-11] + forbiddenTokensSet.add(token); + emit ForbidToken(token); // I:[CC-11] } } - /// @dev Marks the token as limited, which enables quota logic and additional interest for it + /// @notice Marks the token as limited, which enables quota logic and additional interest for it /// @param token Token to make limited - /// @notice This action is irreversible! + /// @dev This action is irreversible! function makeTokenQuoted(address token) external configuratorOnly { // Verifies whether the quota keeper has a token registered as quotable - IPoolQuotaKeeper quotaKeeper = creditManager.poolQuotaKeeper(); + address quotaKeeper = creditManager.poolQuotaKeeper(); - if (!quotaKeeper.isQuotedToken(token)) { + if (!IPoolQuotaKeeper(quotaKeeper).isQuotedToken(token)) { revert TokenIsNotQuotedException(); } @@ -308,57 +366,57 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { uint256 tokenMask = _getAndCheckTokenMaskForSettingLT(token); // Gets current limited mask - uint256 quotedTokenMask = creditManager.quotedTokenMask(); + uint256 quotedTokensMask = creditManager.quotedTokensMask(); // If the token was not limited before, flips the corresponding bit in the mask, // otherwise no actions done. - if (quotedTokenMask & tokenMask == 0) { - quotedTokenMask |= tokenMask; - creditManager.setQuotedMask(quotedTokenMask); + if (quotedTokensMask & tokenMask == 0) { + quotedTokensMask |= tokenMask; + creditManager.setQuotedMask(quotedTokensMask); emit QuoteToken(token); } } - /// @dev Sanity check to verify that the token is a collateral token and + /// @notice Sanity check to verify that the token is a collateral token and /// is not the underlying function _getAndCheckTokenMaskForSettingLT(address token) internal view returns (uint256 tokenMask) { // Gets tokenMask for the token - tokenMask = creditManager.getTokenMaskOrRevert(token); // F:[CC-7] + tokenMask = creditManager.getTokenMaskOrRevert(token); // I:[CC-7] // tokenMask can't be 0, since this means that the token is not a collateral token // tokenMask can't be 1, since this mask is reserved for underlying if (tokenMask == 1) { revert TokenNotAllowedException(); - } // F:[CC-7] + } // I:[CC-7] } // // CONFIGURATION: CONTRACTS & ADAPTERS MANAGEMENT // - /// @dev Adds pair [contract <-> adapter] to the list of allowed contracts + /// @notice Adds pair [contract <-> adapter] to the list of allowed contracts /// or updates adapter address if a contract already has a connected adapter /// @param targetContract Address of allowed contract /// @param adapter Adapter address function allowContract(address targetContract, address adapter) external override - configuratorOnly // F:[CC-2] + configuratorOnly // I:[CC-2] { _allowContract(targetContract, adapter); } - /// @dev IMPLEMENTATION: allowContract + /// @notice IMPLEMENTATION: allowContract function _allowContract(address targetContract, address adapter) internal nonZeroAddress(targetContract) { // Checks that targetContract or adapter != address(0) if (!targetContract.isContract()) { revert AddressIsNotContractException(targetContract); - } // F:[CC-12A] + } // I:[CC-12A] // Checks that the adapter is an actual contract and has the correct Credit Manager and is an actual contract - _revertIfContractIncompatible(adapter); // F:[CC-12] + _revertIfContractIncompatible(adapter); // I:[CC-12] // Additional check that adapter or targetContract is not // creditManager or creditFacade. @@ -367,108 +425,85 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { if ( targetContract == address(creditManager) || targetContract == address(creditFacade()) || adapter == address(creditManager) || adapter == address(creditFacade()) - ) revert TargetContractNotAllowedException(); // F:[CC-13] + ) revert TargetContractNotAllowedException(); // I:[CC-13] // Checks that adapter is not used for another target if (creditManager.adapterToContract(adapter) != address(0)) { revert AdapterUsedTwiceException(); - } // F:[CC-14] + } // I:[CC-14] // If there is an existing adapter for the target contract, it has to be removed address currentAdapter = creditManager.contractToAdapter(targetContract); if (currentAdapter != address(0)) { - creditManager.setContractAllowance(currentAdapter, address(0)); // F: [CC-15A] + creditManager.setContractAllowance({adapter: currentAdapter, targetContract: address(0)}); // I:[CC-15A] } // Sets a link between adapter and targetContract in creditFacade and creditManager - creditManager.setContractAllowance(adapter, targetContract); // F:[CC-15] + creditManager.setContractAllowance({adapter: adapter, targetContract: targetContract}); // I:[CC-15] // adds contract to the list of allowed contracts - allowedContractsSet.add(targetContract); // F:[CC-15] + allowedContractsSet.add(targetContract); // I:[CC-15] - emit AllowContract(targetContract, adapter); // F:[CC-15] + emit AllowContract(targetContract, adapter); // I:[CC-15] } - /// @dev Forbids contract as a target for calls from Credit Accounts + /// @notice Forbids contract as a target for calls from Credit Accounts /// Internally, mappings that determine the adapter <> targetContract link /// Are reset to zero addresses /// @param targetContract Address of a contract to be forbidden function forbidContract(address targetContract) external override - controllerOnly // F:[CC-2B] - nonZeroAddress(targetContract) // F:[CC-12] + controllerOnly // I:[CC-2B] + nonZeroAddress(targetContract) // I:[CC-12] { // Checks that targetContract has a connected adapter address adapter = creditManager.contractToAdapter(targetContract); if (adapter == address(0)) { revert ContractIsNotAnAllowedAdapterException(); - } // F:[CC-16] + } // I:[CC-16] // Sets both contractToAdapter[targetContract] and adapterToContract[adapter] // To address(0), which would make Credit Manager revert on attempts to // call the respective targetContract using the adapter - creditManager.setContractAllowance(adapter, address(0)); // F:[CC-17] - creditManager.setContractAllowance(address(0), targetContract); // F:[CC-17] + creditManager.setContractAllowance({adapter: adapter, targetContract: address(0)}); // I:[CC-17] + creditManager.setContractAllowance({adapter: address(0), targetContract: targetContract}); // I:[CC-17] // removes contract from the list of allowed contracts - allowedContractsSet.remove(targetContract); // F:[CC-17] + allowedContractsSet.remove(targetContract); // I:[CC-17] - emit ForbidContract(targetContract); // F:[CC-17] - } - - /// @dev Removes the link between passed adapter and its contract - /// Useful to remove "orphaned" adapters, i.e. adapters that were replaced but still point - /// to the contract for some reason. This allows users to still execute actions through the old adapter, - /// even though that is not intended. - function forbidAdapter(address adapter) - external - override - configuratorOnly - nonZeroAddress(adapter) // F: [CC-40] - { - /// If the adapter already has no linked target contract, then there is nothing to change - address targetContract = creditManager.adapterToContract(adapter); - if (targetContract == address(0)) { - revert ContractIsNotAnAllowedAdapterException(); // F: [CC-40] - } - - /// Removes the adapter => target contract link only - creditManager.setContractAllowance(adapter, address(0)); // F: [CC-40] - - emit ForbidAdapter(adapter); // F: [CC-40] + emit ForbidContract(targetContract); // I:[CC-17] } // // CREDIT MANAGER MGMT // - /// @dev Sets borrowed amount limits in Credit Facade + /// @notice Sets borrowed amount limits in Credit Facade /// @param _minBorrowedAmount Minimum borrowed amount /// @param _maxBorrowedAmount Maximum borrowed amount function setLimits(uint128 _minBorrowedAmount, uint128 _maxBorrowedAmount) external - controllerOnly // F:[CC-2B] + controllerOnly // I:[CC-2B] { _setLimits(_minBorrowedAmount, _maxBorrowedAmount); } - /// @dev IMPLEMENTATION: setLimits + /// @notice IMPLEMENTATION: setLimits function _setLimits(uint128 _minBorrowedAmount, uint128 _maxBorrowedAmount) internal { // Performs sanity checks on limits: // maxBorrowedAmount must not be less than minBorrowedAmount - // maxBorrowedAmount must not be larger than maximal borrowed amount per block uint8 maxDebtPerBlockMultiplier = creditFacade().maxDebtPerBlockMultiplier(); if (_minBorrowedAmount > _maxBorrowedAmount) { revert IncorrectLimitsException(); - } // F:[CC-18] + } // I:[CC-18] // Sets limits in Credit Facade - creditFacade().setDebtLimits(_minBorrowedAmount, _maxBorrowedAmount, maxDebtPerBlockMultiplier); // F:[CC-19] - emit SetBorrowingLimits(_minBorrowedAmount, _maxBorrowedAmount); // F:[CC-1A,19] + creditFacade().setDebtLimits(_minBorrowedAmount, _maxBorrowedAmount, maxDebtPerBlockMultiplier); // I:[CC-19] + emit SetBorrowingLimits(_minBorrowedAmount, _maxBorrowedAmount); // I:[CC-1A,19] } - /// @dev Sets fees for creditManager + /// @notice Sets fees for creditManager /// @param _feeInterest Percent which protocol charges additionally for interest rate /// @param _feeLiquidation The fee that is paid to the pool from liquidation /// @param _liquidationPremium Discount for totalValue which is given to liquidator @@ -482,7 +517,7 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { uint16 _liquidationPremiumExpired ) external - configuratorOnly // F:[CC-2] + configuratorOnly // I:[CC-2] { // Checks that feeInterest and (liquidationPremium + feeLiquidation) are in range [0..10000] if ( @@ -490,7 +525,7 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { || (_liquidationPremiumExpired + _feeLiquidationExpired) >= PERCENTAGE_FACTOR ) revert IncorrectParameterException(); // FT:[CC-23] - _setParams( + _setFees( _feeInterest, _feeLiquidation, PERCENTAGE_FACTOR - _liquidationPremium, @@ -499,8 +534,9 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { ); // FT:[CC-24,25,26] } - /// @dev Does sanity checks on fee params and sets them in CreditManagerV3 - function _setParams( + /// @notice IMPLEMENTATION: setFees + /// Does sanity checks on fee params and sets them in CreditManagerV3 + function _setFees( uint16 _feeInterest, uint16 _feeLiquidation, uint16 _liquidationDiscount, @@ -508,13 +544,12 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { uint16 _liquidationDiscountExpired ) internal { // Computes the underlying LT and updates it if required - uint16 newLTUnderlying = uint16(_liquidationDiscount - _feeLiquidation); // FT:[CC-25] - (, uint16 ltUnderlying) = creditManager.collateralTokens(0); + (, uint16 ltUnderlying) = creditManager.collateralTokensByMask(UNDERLYING_TOKEN_MASK); if (newLTUnderlying != ltUnderlying) { - _updateLiquidationThreshold(newLTUnderlying); // F:[CC-25] - emit SetTokenLiquidationThreshold(underlying, newLTUnderlying); // F: [CC-1A,25] + _updateLiquidationThreshold(newLTUnderlying); // I:[CC-25] + emit SetTokenLiquidationThreshold(underlying, newLTUnderlying); // I:[CC-1A,25] } ( @@ -533,9 +568,13 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { || (_liquidationDiscountExpired != _liquidationDiscountExpiredCurrent) ) { // updates params in creditManager - creditManager.setParams( - _feeInterest, _feeLiquidation, _liquidationDiscount, _feeLiquidationExpired, _liquidationDiscountExpired - ); + creditManager.setFees({ + _feeInterest: _feeInterest, + _feeLiquidation: _feeLiquidation, + _liquidationDiscount: _liquidationDiscount, + _feeLiquidationExpired: _feeLiquidationExpired, + _liquidationDiscountExpired: _liquidationDiscountExpired + }); emit FeesUpdated( _feeInterest, @@ -547,19 +586,20 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { } } - /// @dev Updates Liquidation threshold for the underlying asset + /// @notice Updates Liquidation threshold for the underlying asset /// @param ltUnderlying New LT for the underlying function _updateLiquidationThreshold(uint16 ltUnderlying) internal { - creditManager.setCollateralTokenData(underlying, ltUnderlying, ltUnderlying, type(uint40).max, 0); // F:[CC-25] + creditManager.setCollateralTokenData(underlying, ltUnderlying, ltUnderlying, type(uint40).max, 0); // I:[CC-25] // An LT of an ordinary collateral token cannot be larger than the LT of underlying // As such, all LTs need to be checked and reduced if needed + // NB: This action will interrupt all ongoing LT ramps uint256 len = creditManager.collateralTokensCount(); unchecked { for (uint256 i = 1; i < len; ++i) { - (address token, uint16 lt) = creditManager.collateralTokens(i); + (address token, uint16 lt) = creditManager.collateralTokensByMask(1 << i); if (lt > ltUnderlying) { - _setLiquidationThreshold(token, ltUnderlying); // F:[CC-25] + _setLiquidationThreshold(token, ltUnderlying); // I:[CC-25] } } } @@ -569,28 +609,28 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { // CONTRACT UPGRADES // - /// @dev Upgrades the price oracle in the Credit Manager, taking the address + /// @notice Upgrades the price oracle in the Credit Manager, taking the address /// from the address provider - function setPriceOracle() + function setPriceOracle(uint256 _version) external - configuratorOnly // F:[CC-2] + configuratorOnly // I:[CC-2] { - address priceOracle = addressProvider.getPriceOracle(); + address priceOracle = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_PRICE_ORACLE, _version); address currentPriceOracle = address(creditManager.priceOracle()); // Checks that the price oracle is actually new to avoid emitting redundant events if (priceOracle != currentPriceOracle) { - creditManager.setPriceOracle(priceOracle); // F: [CC-28] - emit SetPriceOracle(priceOracle); // F:[CC-28] + creditManager.setPriceOracle(priceOracle); // I:[CC-28] + emit SetPriceOracle(priceOracle); // I:[CC-28] } } - /// @dev Upgrades the Credit Facade corresponding to the Credit Manager + /// @notice Upgrades the Credit Facade corresponding to the Credit Manager /// @param _creditFacade address of the new CreditFacadeV3 /// @param migrateParams Whether the previous CreditFacadeV3's parameter need to be copied function setCreditFacade(address _creditFacade, bool migrateParams) external - configuratorOnly // F:[CC-2] + configuratorOnly // I:[CC-2] { // Checks that the Credit Facade is actually changed, to avoid // any redundant actions and events @@ -599,7 +639,7 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { } // Sanity checks that the address is a contract and has correct Credit Manager - _revertIfContractIncompatible(_creditFacade); // F:[CC-29] + _revertIfContractIncompatible(_creditFacade); // I:[CC-29] // Retrieves all parameters in case they need // to be migrated @@ -612,73 +652,115 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { bool expirable = creditFacade().expirable(); - address botList = creditFacade().botList(); + uint256 botListVersion; + { + address botList = creditFacade().botList(); + botListVersion = botList == address(0) ? 0 : IVersion(botList).version(); + } + + (, uint128 maxCumulativeLoss) = creditFacade().lossParams(); // Sets Credit Facade to the new address - creditManager.setCreditFacade(_creditFacade); // F:[CC-30] + creditManager.setCreditFacade(_creditFacade); // I:[CC-30] if (migrateParams) { // Copies all limits and restrictions on borrowing - _setMaxDebtPerBlockMultiplier(_maxDebtPerBlockMultiplier); // F:[CC-30] - _setLimits(minBorrowedAmount, maxBorrowedAmount); // F:[CC-30] + _setMaxDebtPerBlockMultiplier(_maxDebtPerBlockMultiplier); // I:[CC-30] + _setLimits(minBorrowedAmount, maxBorrowedAmount); // I:[CC-30] + _setMaxCumulativeLoss(maxCumulativeLoss); + + // Migrates array-based parameters + _migrateEmergencyLiquidators(); + _migrateForbiddenTokens(); // Copies the expiration date if the contract is expirable - if (expirable) _setExpirationDate(expirationDate); // F: [CC-30] + if (expirable) _setExpirationDate(expirationDate); // I:[CC-30] + + if (botListVersion != 0) _setBotList(botListVersion); + } + + emit SetCreditFacade(_creditFacade); // I:[CC-30] + } - if (botList != address(0)) _setBotList(botList); + /// @notice Internal function to migrate emergency liquidators when + /// updating the Credit Facade + function _migrateEmergencyLiquidators() internal { + uint256 len = emergencyLiquidatorsSet.length(); + for (uint256 i; i < len;) { + _addEmergencyLiquidator(emergencyLiquidatorsSet.at(i)); + unchecked { + ++i; + } } + } - emit SetCreditFacade(_creditFacade); // F:[CC-30] + /// @notice Internal function to migrate forbidden tokens when + /// updating the Credit Facade + function _migrateForbiddenTokens() internal { + uint256 len = forbiddenTokensSet.length(); + for (uint256 i; i < len;) { + _forbidToken(forbiddenTokensSet.at(i)); + unchecked { + ++i; + } + } } - /// @dev Upgrades the Credit Configurator for a connected Credit Manager + /// @notice Upgrades the Credit Configurator for a connected Credit Manager /// @param _creditConfigurator New Credit Configurator's address - /// @notice After this function executes, this Credit Configurator no longer + /// @dev After this function executes, this Credit Configurator no longer /// has admin access to the Credit Manager function upgradeCreditConfigurator(address _creditConfigurator) external - configuratorOnly // F:[CC-2] + configuratorOnly // I:[CC-2] { if (_creditConfigurator == address(this)) { return; } - _revertIfContractIncompatible(_creditConfigurator); // F:[CC-29] + _revertIfContractIncompatible(_creditConfigurator); // I:[CC-29] - creditManager.setCreditConfigurator(_creditConfigurator); // F:[CC-31] - emit CreditConfiguratorUpgraded(_creditConfigurator); // F:[CC-31] + creditManager.setCreditConfigurator(_creditConfigurator); // I:[CC-31] + emit CreditConfiguratorUpgraded(_creditConfigurator); // I:[CC-31] } - /// @dev Performs sanity checks that the address is a contract compatible + /// @notice Performs sanity checks that the address is a contract compatible /// with the current Credit Manager function _revertIfContractIncompatible(address _contract) internal view - nonZeroAddress(_contract) // F:[CC-12,29] + nonZeroAddress(_contract) // I:[CC-12,29] { // Checks that the address is a contract if (!_contract.isContract()) { revert AddressIsNotContractException(_contract); - } // F:[CC-12A,29] + } // I:[CC-12A,29] // Checks that the contract has a creditManager() function, which returns a correct value - try CreditFacadeV3(_contract).creditManager() returns (ICreditManagerV3 cm) { - if (cm != creditManager) revert IncompatibleContractException(); // F:[CC-12B,29] + try CreditFacadeV3(_contract).creditManager() returns (address cm) { + if (cm != address(creditManager)) revert IncompatibleContractException(); // I:[CC-12B,29] } catch { - revert IncompatibleContractException(); // F:[CC-12B,29] + revert IncompatibleContractException(); // I:[CC-12B,29] } } - /// @dev Disables borrowing in Credit Facade (and, consequently, the Credit Manager) + /// @notice Disables borrowing in Credit Facade (and, consequently, the Credit Manager) function forbidBorrowing() external pausableAdminsOnly { + /// This is done by setting the max debt per block multiplier to 0, + /// which prevents all new borrowing _setMaxDebtPerBlockMultiplier(0); } - /// @dev Sets the max cumulative loss, which is a threshold of total loss that triggers a system pause + /// @notice Sets the max cumulative loss, which is a threshold of total loss that triggers a system pause function setMaxCumulativeLoss(uint128 _maxCumulativeLoss) external - configuratorOnly // F: [CC-02] + configuratorOnly // I:[CC-02] { + _setMaxCumulativeLoss(_maxCumulativeLoss); + } + + /// @notice IMPLEMENTATION: setMaxCumulativeLoss + function _setMaxCumulativeLoss(uint128 _maxCumulativeLoss) internal { (, uint128 maxCumulativeLossCurrent) = creditFacade().lossParams(); if (_maxCumulativeLoss != maxCumulativeLossCurrent) { @@ -687,49 +769,49 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { } } - /// @dev Resets the current cumulative loss + /// @notice Resets the current cumulative loss function resetCumulativeLoss() external - configuratorOnly // F: [CC-02] + configuratorOnly // I:[CC-02] { (, uint128 maxCumulativeLossCurrent) = creditFacade().lossParams(); creditFacade().setCumulativeLossParams(maxCumulativeLossCurrent, true); emit ResetCumulativeLoss(); } - /// @dev Sets the maximal borrowed amount per block + /// @notice Sets the maximal borrowed amount per block /// @param newMaxDebtLimitPerBlockMultiplier The new max borrowed amount per block function setMaxDebtPerBlockMultiplier(uint8 newMaxDebtLimitPerBlockMultiplier) external - controllerOnly // F:[CC-2B] + controllerOnly // I:[CC-2B] { - _setMaxDebtPerBlockMultiplier(newMaxDebtLimitPerBlockMultiplier); // F:[CC-33] + _setMaxDebtPerBlockMultiplier(newMaxDebtLimitPerBlockMultiplier); // I:[CC-33] } - /// @dev IMPLEMENTATION: _setMaxDebtPerBlockMultiplier + /// @notice IMPLEMENTATION: _setMaxDebtPerBlockMultiplier function _setMaxDebtPerBlockMultiplier(uint8 newMaxDebtLimitPerBlockMultiplier) internal { uint8 _maxDebtPerBlockMultiplier = creditFacade().maxDebtPerBlockMultiplier(); (uint128 minBorrowedAmount, uint128 maxBorrowedAmount) = creditFacade().debtLimits(); // Checks that the limit was actually changed to avoid redundant events if (newMaxDebtLimitPerBlockMultiplier != _maxDebtPerBlockMultiplier) { - creditFacade().setDebtLimits(minBorrowedAmount, maxBorrowedAmount, newMaxDebtLimitPerBlockMultiplier); // F:[CC-33] - emit SetMaxDebtPerBlockMultiplier(newMaxDebtLimitPerBlockMultiplier); // F:[CC-1A,33] + creditFacade().setDebtLimits(minBorrowedAmount, maxBorrowedAmount, newMaxDebtLimitPerBlockMultiplier); // I:[CC-33] + emit SetMaxDebtPerBlockMultiplier(newMaxDebtLimitPerBlockMultiplier); // I:[CC-1A,33] } } - /// @dev Sets expiration date in a CreditFacadeV3 connected + /// @notice Sets expiration date in a CreditFacadeV3 connected /// To a CreditManagerV3 with an expirable pool /// @param newExpirationDate The timestamp of the next expiration - /// @notice See more at https://dev.gearbox.fi/docs/documentation/credit/liquidation#liquidating-accounts-by-expiration + /// @dev See more at https://dev.gearbox.fi/docs/documentation/credit/liquidation#liquidating-accounts-by-expiration function setExpirationDate(uint40 newExpirationDate) external - configuratorOnly // F: [CC-38] + configuratorOnly // I:[CC-38] { - _setExpirationDate(newExpirationDate); // F: [CC-34] + _setExpirationDate(newExpirationDate); // I:[CC-34] } - /// @dev IMPLEMENTATION: setExpirationDate + /// @notice IMPLEMENTATION: setExpirationDate function _setExpirationDate(uint40 newExpirationDate) internal { uint40 expirationDate = creditFacade().expirationDate(); @@ -738,37 +820,41 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { // The new expiration date cannot be earlier than now if (expirationDate >= newExpirationDate || block.timestamp > newExpirationDate) { revert IncorrectExpirationDateException(); - } // F: [CC-34] + } // I:[CC-34] - creditFacade().setExpirationDate(newExpirationDate); // F: [CC-34] - emit SetExpirationDate(newExpirationDate); // F: [CC-34] + creditFacade().setExpirationDate(newExpirationDate); // I:[CC-34] + emit SetExpirationDate(newExpirationDate); // I:[CC-34] } - /// @dev Sets the maximal amount of enabled tokens per Credit Account + /// @notice Sets the maximal amount of enabled tokens per Credit Account /// @param maxEnabledTokens The new maximal number of enabled tokens - /// @notice A large number of enabled collateral tokens on a Credit Account + /// @dev A large number of enabled collateral tokens on a Credit Account /// can make liquidations and health checks prohibitively expensive in terms of gas, /// hence the number is limited function setMaxEnabledTokens(uint8 maxEnabledTokens) external - controllerOnly // F:[CC-2B] + controllerOnly // I:[CC-2B] { - uint256 maxEnabledTokensCurrent = creditManager.maxAllowedEnabledTokenLength(); + uint256 maxEnabledTokensCurrent = creditManager.maxEnabledTokens(); // Checks that value is actually changed, to avoid redundant checks if (maxEnabledTokens != maxEnabledTokensCurrent) { - creditManager.setMaxEnabledTokens(maxEnabledTokens); // F: [CC-37] - emit SetMaxEnabledTokens(maxEnabledTokens); // F: [CC-37] + creditManager.setMaxEnabledTokens(maxEnabledTokens); // I:[CC-37] + emit SetMaxEnabledTokens(maxEnabledTokens); // I:[CC-37] } } - /// @dev Sets the bot list contract - /// @param botList The address of the new bot list - function setBotList(address botList) external configuratorOnly { - _setBotList(botList); + /// @notice Sets the bot list contract + /// @param version The version of the new bot list contract + /// The contract address is retrieved from addressProvider + /// @notice The bot list determines the permissions for actions + /// that bots can perform on Credit Accounts + function setBotList(uint256 version) external configuratorOnly { + _setBotList(version); } - function _setBotList(address botList) internal nonZeroAddress(botList) { + function _setBotList(uint256 version) internal { + address botList = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_BOT_LIST, version); address currentBotList = creditFacade().botList(); if (botList != currentBotList) { @@ -777,50 +863,52 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { } } - /// @dev Adds an address to the list of emergency liquidators + /// @notice Adds an address to the list of emergency liquidators /// @param liquidator The address to add to the list - /// @notice Emergency liquidators are trusted addresses + /// @dev Emergency liquidators are trusted addresses /// that are able to liquidate positions while the contracts are paused, /// e.g. when there is a risk of bad debt while an exploit is being patched. /// In the interest of fairness, emergency liquidators do not receive a premium /// And are compensated by the Gearbox DAO separately. function addEmergencyLiquidator(address liquidator) external - configuratorOnly // F: [CC-38] + configuratorOnly // I:[CC-38] { _addEmergencyLiquidator(liquidator); } - /// @dev IMPLEMENTATION: addEmergencyLiquidator + /// @notice IMPLEMENTATION: addEmergencyLiquidator function _addEmergencyLiquidator(address liquidator) internal { bool statusCurrent = creditFacade().canLiquidateWhilePaused(liquidator); // Checks that the address is not already in the list, // to avoid redundant events if (!statusCurrent) { - creditFacade().setEmergencyLiquidator(liquidator, AllowanceAction.ALLOW); // F: [CC-38] - emit AddEmergencyLiquidator(liquidator); // F: [CC-38] + creditFacade().setEmergencyLiquidator(liquidator, AllowanceAction.ALLOW); // I:[CC-38] + emergencyLiquidatorsSet.add(liquidator); + emit AddEmergencyLiquidator(liquidator); // I:[CC-38] } } - /// @dev Removex an address frp, the list of emergency liquidators + /// @notice Removex an address frp, the list of emergency liquidators /// @param liquidator The address to remove from the list function removeEmergencyLiquidator(address liquidator) external - configuratorOnly // F: [CC-38] + configuratorOnly // I:[CC-38] { _removeEmergencyLiquidator(liquidator); } - /// @dev IMPLEMENTATION: removeEmergencyLiquidator + /// @notice IMPLEMENTATION: removeEmergencyLiquidator function _removeEmergencyLiquidator(address liquidator) internal { bool statusCurrent = creditFacade().canLiquidateWhilePaused(liquidator); // Checks that the address is in the list // to avoid redundant events if (statusCurrent) { - creditFacade().setEmergencyLiquidator(liquidator, AllowanceAction.FORBID); // F: [CC-38] - emit RemoveEmergencyLiquidator(liquidator); // F: [CC-38] + creditFacade().setEmergencyLiquidator(liquidator, AllowanceAction.FORBID); // I:[CC-38] + emergencyLiquidatorsSet.remove(liquidator); + emit RemoveEmergencyLiquidator(liquidator); // I:[CC-38] } } @@ -828,7 +916,7 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { // GETTERS // - /// @dev Returns all allowed contracts + /// @notice Returns all allowed contracts function allowedContracts() external view override returns (address[] memory result) { uint256 len = allowedContractsSet.length(); result = new address[](len); @@ -840,7 +928,31 @@ contract CreditConfigurator is ICreditConfigurator, ACLNonReentrantTrait { } } - /// @dev Returns the Credit Facade currently connected to the Credit Manager + /// @notice Returns all emergency liquidators + function emergencyLiquidators() external view override returns (address[] memory result) { + uint256 len = emergencyLiquidatorsSet.length(); + result = new address[](len); + for (uint256 i; i < len;) { + result[i] = emergencyLiquidatorsSet.at(i); + unchecked { + ++i; + } + } + } + + /// @notice Returns all forbidden tokens + function forbiddenTokens() external view override returns (address[] memory result) { + uint256 len = forbiddenTokensSet.length(); + result = new address[](len); + for (uint256 i; i < len;) { + result[i] = forbiddenTokensSet.at(i); + unchecked { + ++i; + } + } + } + + /// @notice Returns the Credit Facade currently connected to the Credit Manager function creditFacade() public view override returns (CreditFacadeV3) { return CreditFacadeV3(creditManager.creditFacade()); } diff --git a/contracts/credit/CreditFacadeV3.sol b/contracts/credit/CreditFacadeV3.sol index 48c418e7..b6041c4c 100644 --- a/contracts/credit/CreditFacadeV3.sol +++ b/contracts/credit/CreditFacadeV3.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; +import "../interfaces/IAddressProviderV3.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; // LIBS & TRAITS -import {CreditLogic} from "../libraries/CreditLogic.sol"; +import {BalancesLogic} from "../libraries/BalancesLogic.sol"; import {ACLNonReentrantTrait} from "../traits/ACLNonReentrantTrait.sol"; import {BitMask, UNDERLYING_TOKEN_MASK} from "../libraries/BitMask.sol"; @@ -27,10 +28,10 @@ import { } from "../interfaces/ICreditManagerV3.sol"; import {AllowanceAction} from "../interfaces/ICreditConfiguratorV3.sol"; import {ClaimAction} from "../interfaces/IWithdrawalManager.sol"; - import {IPriceOracleV2} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; +import {IPriceFeedOnDemand} from "../interfaces/IPriceFeedOnDemand.sol"; -import {IPool4626} from "../interfaces/IPool4626.sol"; +import {IPoolV3} from "../interfaces/IPoolV3.sol"; import {IDegenNFT} from "@gearbox-protocol/core-v2/contracts/interfaces/IDegenNFT.sol"; import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; import {IWETHGateway} from "../interfaces/IWETHGateway.sol"; @@ -43,165 +44,154 @@ import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/P // EXCEPTIONS import "../interfaces/IExceptions.sol"; -import "forge-std/console.sol"; - uint256 constant OPEN_CREDIT_ACCOUNT_FLAGS = ALL_PERMISSIONS & ~(INCREASE_DEBT_PERMISSION | DECREASE_DEBT_PERMISSION | WITHDRAW_PERMISSION) | INCREASE_DEBT_WAS_CALLED; uint256 constant CLOSE_CREDIT_ACCOUNT_FLAGS = EXTERNAL_CALLS_PERMISSION; -struct DebtLimits { - /// @dev Minimal borrowed amount per credit account - uint128 minDebt; - /// @dev Maximum aborrowed amount per credit account - uint128 maxDebt; -} - -struct CumulativeLossParams { - /// @dev Current cumulative loss from all bad debt liquidations - uint128 currentCumulativeLoss; - /// @dev Max cumulative loss accrued before the system is paused - uint128 maxCumulativeLoss; -} - /// @title CreditFacadeV3 -/// @notice User interface for interacting with Credit Manager. +/// @notice A contract that provides a user interface for interacting with Credit Manager. /// @dev CreditFacadeV3 provides an interface between the user and the Credit Manager. Direct interactions -/// with the Credit Manager are forbidden. There are two ways the Credit Manager can be interacted with: -/// - Through CreditFacadeV3, which provides all the required account management function: open / close / liquidate / manageDebt, -/// as well as Multicalls that allow to perform multiple actions within a single transaction, with a single health check -/// - Through adapters, which call the Credit Manager directly, but only allow interactions with specific target contracts +/// with the Credit Manager are forbidden. Credit Facade provides access to all account management functions, +/// opening, closing, liquidating, managing debt, as well as calls to external protocols (through adapters, which +/// also can't be interacted with directly). All of these actions are only accessible through `multicall`. contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { using Address for address; using BitMask for uint256; - /// @dev Credit Manager connected to this Credit Facade - ICreditManagerV3 public immutable creditManager; + /// @notice Credit Manager connected to this Credit Facade + address public immutable creditManager; - /// @dev Whether the whitelisted mode is active + /// @notice Whether the whitelisted mode is active bool public immutable whitelisted; - /// @dev Whether the Credit Facade implements expirable logic + /// @notice Whether the Credit Facade implements expirable logic bool public immutable expirable; - /// @dev Address of the pool - address public immutable pool; - - /// @dev Address of the underlying token - address public immutable underlying; + /// @notice Address of WETH + address public immutable weth; - /// @dev Address of WETH - address public immutable wethAddress; + /// @notice Address of WETH Gateway + address public immutable wethGateway; - /// @dev Address of WETH Gateway - IWETHGateway public immutable wethGateway; - - /// @dev Address of the DegenNFT that gatekeeps account openings in whitelisted mode + /// @notice Address of the DegenNFT that gatekeeps account openings in whitelisted mode address public immutable override degenNFT; - /// @dev Keeps borrowing debtLimits together for storage access optimization - DebtLimits public debtLimits; + /// @notice Date of the next Credit Account expiration (for CF's with expirable logic) + uint40 public expirationDate; - /// @dev Maximal amount of new debt that can be taken per block + /// @notice Maximal amount of new debt that can be taken per block uint8 public override maxDebtPerBlockMultiplier; - /// @dev Stores in a compressed state the last block where borrowing happened and the total amount borrowed in that block + /// @notice Last block in which debt was increased on a Credit Account + uint64 internal lastBlockBorrowed; + + /// @notice The total amount of new debt in the last block where debt was increased uint128 internal totalBorrowedInBlock; - uint64 internal lastBlockBorrowed; + /// @notice Contract containing permissions from borrowers to bots + address public botList; + + /// @notice Limits on debt principal for a single Credit Account + DebtLimits public debtLimits; - /// @dev Bit mask encoding a set of forbidden tokens + /// @notice Bit mask encoding a set of forbidden tokens uint256 public forbiddenTokenMask; - /// @dev Keeps parameters that are used to pause the system after too much bad debt over a short period + /// @notice Keeps parameters that are used to pause the system after too much bad debt over a short period CumulativeLossParams public override lossParams; - /// @dev Contract containing the list of approval statuses for borrowers / bots - address public botList; - - /// @dev - uint40 public expirationDate; - - /// @dev A map that stores whether a user allows a transfer of an account from another user to themselves + /// @notice A map that stores whether a user allows a transfer of an account from another user to themselves mapping(address => mapping(address => bool)) public override transfersAllowed; - /// @dev Maps addresses to their status as emergency liquidator. - /// @notice Emergency liquidators are trusted addresses + /// @notice Maps addresses to their status as emergency liquidator. + /// @dev Emergency liquidators are trusted addresses /// that are able to liquidate positions while the contracts are paused, /// e.g. when there is a risk of bad debt while an exploit is being patched. /// In the interest of fairness, emergency liquidators do not receive a premium /// And are compensated by the Gearbox DAO separately. mapping(address => bool) public override canLiquidateWhilePaused; - /// @dev Contract version + /// @notice Contract version uint256 public constant override version = 3_00; - /// @dev Restricts actions for users with opened credit accounts only + /// @notice Restricts functions to the connected Credit Configurator only modifier creditConfiguratorOnly() { - if (msg.sender != creditManager.creditConfigurator()) { + _checkCreditConfigurator(); + _; + } + + /// @notice Private function for `creditConfiguratorOnly`; used for contract size optimization + function _checkCreditConfigurator() private view { + if (msg.sender != ICreditManagerV3(creditManager).creditConfigurator()) { revert CallerNotConfiguratorException(); } - - _; } + /// @notice Restricts functions to the owner of a Credit Account modifier creditAccountOwnerOnly(address creditAccount) { - if (msg.sender != _getBorrowerOrRevert(creditAccount)) { - revert CallerNotCreditAccountOwnerException(); - } + _checkCreditAccountOwner(creditAccount); _; } - modifier nonZeroCallsOnly(MultiCall[] calldata calls) { - if (calls.length == 0) { - revert ZeroCallsException(); + /// @notice Private function for `creditAccountOwnerOnly`; used for contract size optimization + function _checkCreditAccountOwner(address creditAccount) private view { + if (msg.sender != _getBorrowerOrRevert(creditAccount)) { + revert CallerNotCreditAccountOwnerException(); } - _; } + /// @notice Restricts functions to the non-paused contract state, unless the caller + /// is an emergency liquidator modifier whenNotPausedOrEmergency() { require(!paused() || canLiquidateWhilePaused[msg.sender], "Pausable: paused"); _; } - // Reverts if CreditFacadeV3 is expired + /// @notice Restricts functions to when the CF is not expired modifier whenNotExpired() { + _checkExpired(); + _; + } + + /// @notice Reverts if the contract is expired + function _checkExpired() private view { if (_isExpired()) { revert NotAllowedAfterExpirationException(); // F: [FA-46] } - _; } - /// @dev Initializes creditFacade and connects it with CreditManagerV3 + /// @notice Initializes creditFacade and connects it to CreditManagerV3 /// @param _creditManager address of Credit Manager /// @param _degenNFT address of the DegenNFT or address(0) if whitelisted mode is not used /// @param _expirable Whether the CreditFacadeV3 can expire and implements expiration-related logic constructor(address _creditManager, address _degenNFT, bool _expirable) - ACLNonReentrantTrait(address(IPool4626(ICreditManagerV3(_creditManager).pool()).addressProvider())) - nonZeroAddress(_creditManager) + ACLNonReentrantTrait(ICreditManagerV3(_creditManager).addressProvider()) { - creditManager = ICreditManagerV3(_creditManager); // F:[FA-1A] - pool = creditManager.pool(); - underlying = ICreditManagerV3(_creditManager).underlying(); // F:[FA-1A] + creditManager = _creditManager; // U:[FA-1] // F:[FA-1A] - wethAddress = ICreditManagerV3(_creditManager).wethAddress(); // F:[FA-1A] - wethGateway = IWETHGateway(ICreditManagerV3(_creditManager).wethGateway()); + weth = ICreditManagerV3(_creditManager).weth(); // U:[FA-1] // F:[FA-1A] + wethGateway = ICreditManagerV3(_creditManager).wethGateway(); // U:[FA-1] + botList = + IAddressProviderV3(ICreditManagerV3(_creditManager).addressProvider()).getAddressOrRevert(AP_BOT_LIST, 3_00); - degenNFT = _degenNFT; // F:[FA-1A] - whitelisted = _degenNFT != address(0); // F:[FA-1A] + degenNFT = _degenNFT; // U:[FA-1] // F:[FA-1A] + whitelisted = _degenNFT != address(0); // U:[FA-1] // F:[FA-1A] - expirable = _expirable; + expirable = _expirable; // U:[FA-1] // F:[FA-1A] } // Notice: ETH interactions - // CreditFacadeV3 implements a new flow for interacting with WETH compared to V1. + // CreditFacadeV3 implements the following flow for accepting native ETH: // During all actions, any sent ETH value is automatically wrapped into WETH and // sent back to the message sender. This makes the protocol's behavior regarding // ETH more flexible and consistent, since there is no need to pre-wrap WETH before // interacting with the protocol, and no need to compute how much unused ETH has to be sent back. - /// @dev Opens a Credit Account and runs a batch of operations in a multicall - /// - Opens credit account with the desired borrowed amount + /// @notice Opens a Credit Account and runs a batch of operations in a multicall + /// - Performs sanity checks + /// - Burns DegenNFT (in whitelisted mode) + /// - Opens credit account with the desired debt amount /// - Executes all operations in a multicall /// - Checks that the new account has enough collateral /// - Emits OpenCreditAccount event @@ -211,14 +201,8 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { /// msg.sender != onBehalfOf /// @param calls The array of MultiCall structs encoding the required operations. Generally must have /// at least a call to addCollateral, as otherwise the health check at the end will fail. - /// @param referralCode Referral code which is used for potential rewards. 0 if no referral code provided - function openCreditAccount( - uint256 debt, - address onBehalfOf, - MultiCall[] calldata calls, - bool deployNew, - uint16 referralCode - ) + /// @param referralCode Referral code that is used for potential rewards. 0 if no referral code provided + function openCreditAccount(uint256 debt, address onBehalfOf, MultiCall[] calldata calls, uint16 referralCode) external payable override @@ -226,16 +210,13 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { whenNotExpired nonReentrant nonZeroAddress(onBehalfOf) - nonZeroCallsOnly(calls) returns (address creditAccount) { - uint256[] memory forbiddenBalances; - - // Checks that the borrowed amount is within the borrowing debtLimits + // Checks that the borrowed amount is within the debt limits _revertIfOutOfDebtLimits(debt); // F:[FA-11B] // Checks whether the new borrowed amount does not violate the block limit - _checkIncreaseDebtAllowedAndUpdateBlockLimit(debt); // F:[FA-11] + _revertIfOutOfBorrowingLimit(debt); // F:[FA-11] // Checks that the msg.sender can open an account for onBehalfOf // msg.sender must either be the account owner themselves, or be approved for transfers @@ -243,7 +224,7 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { _revertIfAccountTransferNotAllowed(msg.sender, onBehalfOf); } // F:[FA-04C] - // F:[FA-5] covers case when degenNFT == address(0) + /// Attempts to burn the DegenNFT - if onBehalfOf has none, this will fail if (degenNFT != address(0)) { IDegenNFT(degenNFT).burn(onBehalfOf, 1); // F:[FA-4B] } @@ -252,12 +233,15 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { _wrapETH(); // F:[FA-3B] // Requests the Credit Manager to open a Credit Account - creditAccount = creditManager.openCreditAccount({debt: debt, onBehalfOf: onBehalfOf, deployNew: deployNew}); // F:[FA-8] + creditAccount = ICreditManagerV3(creditManager).openCreditAccount({debt: debt, onBehalfOf: onBehalfOf}); // F:[FA-8] - // emits a new event + // Emits an event for Credit Account opening emit OpenCreditAccount(creditAccount, onBehalfOf, msg.sender, debt, referralCode); // F:[FA-8] - // F:[FA-10]: no free flashloans through opening a Credit Account - // and immediately decreasing debt + + // Initially, only the underlying is on the Credit Account, + // so the enabledTokenMask before the multicall is 1 + // Also, changing debt is prohibited during account opening, + // to prevent any free flash loans FullCheckParams memory fullCheckParams = _multicall({ creditAccount: creditAccount, calls: calls, @@ -265,52 +249,71 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { flags: OPEN_CREDIT_ACCOUNT_FLAGS }); // F:[FA-8] + // Since it's not possible to enable any forbidden tokens on a new account, + // this array is empty + uint256[] memory forbiddenBalances; + // Checks that the new credit account has enough collateral to cover the debt - _fullCollateralCheck( - creditAccount, UNDERLYING_TOKEN_MASK, fullCheckParams, forbiddenBalances, forbiddenTokenMask - ); // F:[FA-8, 9] + _fullCollateralCheck({ + creditAccount: creditAccount, + enabledTokensMaskBefore: UNDERLYING_TOKEN_MASK, + fullCheckParams: fullCheckParams, + forbiddenBalances: forbiddenBalances, + _forbiddenTokenMask: forbiddenTokenMask + }); // F:[FA-8, 9] } - /// @dev Runs a batch of transactions within a multicall and closes the account - /// - Wraps ETH to WETH and sends it msg.sender if value > 0 - /// - Executes the multicall - the main purpose of a multicall when closing is to convert all assets to underlying - /// in order to pay the debt. + /// @notice Runs a batch of transactions within a multicall and closes the account + /// - Retrieves all debt data from the Credit Manager, such as debt and accrued interest and fees + /// - Forces all pending withdrawals, even if they are not mature yet: successful account closure means + /// that there was enough collateral on the account to fully repay all debt - so this action is safe + /// - Executes the multicall - the main purpose of a multicall when closing is to convert assets to underlying + /// in order to pay the debt. + /// - Erases all bot permissions from an account, to protect future users from potentially unwanted bot permissions /// - Closes credit account: /// + Checks the underlying balance: if it is greater than the amount paid to the pool, transfers the underlying - /// from the Credit Account and proceeds. If not, tries to transfer the shortfall from msg.sender. + /// from the Credit Account and proceeds. If not, tries to transfer the shortfall from msg.sender; + /// + If active quotas are present, they are all set to zero; /// + Transfers all enabled assets with non-zero balances to the "to" address, unless they are marked /// to be skipped in skipTokenMask - /// + If there are withdrawals scheduled for Credit Account, claims them all to `to` - /// + If convertWETH is true, converts WETH into ETH before sending to the recipient + /// + If convertToETH is true, converts WETH into ETH before sending to the recipient + /// + Returns the Credit Account to the factory /// - Emits a CloseCreditAccount event /// + /// @param creditAccount Address of the Credit Account to liquidate. This is required, as V3 allows a borrower to + /// have several CAs with one Credit Manager /// @param to Address to send funds to during account closing /// @param skipTokenMask Uint-encoded bit mask where 1's mark tokens that shouldn't be transferred - /// @param convertWETH If true, converts WETH into ETH before sending to "to" + /// @param convertToETH If true, converts WETH into ETH before sending to "to" /// @param calls The array of MultiCall structs encoding the operations to execute before closing the account. function closeCreditAccount( address creditAccount, address to, uint256 skipTokenMask, - bool convertWETH, + bool convertToETH, MultiCall[] calldata calls ) external payable override whenNotPaused creditAccountOwnerOnly(creditAccount) nonReentrant { // Wraps ETH and sends it back to msg.sender _wrapETH(); // F:[FA-3C] + /// Requests CM to calculate debt only, since we don't need to know the collateral value for + /// full account closure CollateralDebtData memory debtData = _calcDebtAndCollateral(creditAccount, CollateralCalcTask.DEBT_ONLY); + /// All pending withdrawals are claimed, even if they are not yet mature _claimWithdrawals(creditAccount, to, ClaimAction.FORCE_CLAIM); - // [FA-13]: Calls to CreditFacadeV3 are forbidden during closure if (calls.length != 0) { // TODO: CHANGE + /// All account management functions are forbidden during closure FullCheckParams memory fullCheckParams = _multicall(creditAccount, calls, debtData.enabledTokensMask, CLOSE_CREDIT_ACCOUNT_FLAGS); debtData.enabledTokensMask = fullCheckParams.enabledTokensMaskAfter; } // F:[FA-2, 12, 13] - /// HOW TO CHECK QUOTED BALANCES + /// Bot permissions are specific to (owner, creditAccount), + /// so they need to be erased on account closure + _eraseAllBotPermissions({creditAccount: creditAccount, setFlag: false}); // Requests the Credit manager to close the Credit Account _closeCreditAccount({ @@ -320,25 +323,33 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { payer: msg.sender, to: to, skipTokensMask: skipTokenMask, - convertWETH: convertWETH + convertToETH: convertToETH }); // F:[FA-2, 12] // TODO: add test - if (convertWETH) { + if (convertToETH) { _wethWithdrawTo(to); } - // Emits a CloseCreditAccount event + // Emits an event emit CloseCreditAccount(creditAccount, msg.sender, to); // F:[FA-12] } - /// @dev Runs a batch of transactions within a multicall and liquidates the account + /// @notice Runs a batch of transactions within a multicall and liquidates the account /// - Computes the total value and checks that hf < 1. An account can't be liquidated when hf >= 1. /// Total value has to be computed before the multicall, otherwise the liquidator would be able - /// to manipulate it. - /// - Wraps ETH to WETH and sends it to msg.sender (liquidator) if value > 0 + /// to manipulate it. Withdrawals are included into the total value according to the following logic + /// + If the liquidation is normal, then only non-mature withdrawals are included. This means + /// that if the CA has enough collateral INCLUDING immature withdrawals, then it is considered healthy. + /// + If the liquidation is emergency, then ALL withdrawals are included. If an attack attempt was performed and + /// the attacker scheduled a malicious withdrawal, this ensures that the funds can be recovered (by force cancelling the withdrawal) + /// even if this withdrawal matures while a response is being coordinated. + /// - Cancels or claims withdrawals based on liquidation type: + /// + If this is a normal liquidation, then mature pending withdrawals are claimed and immature ones are cancelled and returned to the Credit Account + /// + If this is an emergency liquidation, all pending withdrawals (regardless of maturity) are returned to the CA /// - Executes the multicall - the main purpose of a multicall when liquidating is to convert all assets to underlying /// in order to pay the debt. + /// - Erases all bot permissions from an account, to protect future users from potentially unwanted bot permissions /// - Liquidate credit account: /// + Computes the amount that needs to be paid to the pool. If totalValue * liquidationDiscount < borrow + interest + fees, /// only totalValue * liquidationDiscount has to be paid. Since liquidationDiscount < 1, the liquidator can take @@ -349,19 +360,23 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { /// + Transfers all enabled assets with non-zero balances to the "to" address, unless they are marked /// to be skipped in skipTokenMask. If the liquidator is confident that all assets were converted /// during the multicall, they can set the mask to uint256.max - 1, to only transfer the underlying - /// + If there are withdrawals scheduled for Credit Account, cancels immature withdrawals and claims mature ones - /// + If convertWETH is true, converts WETH into ETH before sending + /// + If active quotas are present, they are all set to zero; + /// + If convertToETH is true, converts WETH into ETH before sending + /// + Returns the Credit Account to the factory + /// - If liquidation reported a loss, borrowing is prohibited and the cumulative loss value is increase; + /// If cumulative loss reaches a critical threshold, the system is paused /// - Emits LiquidateCreditAccount event /// + /// @param creditAccount Credit Account to liquidate /// @param to Address to send funds to after liquidation /// @param skipTokenMask Uint-encoded bit mask where 1's mark tokens that shouldn't be transferred - /// @param convertWETH If true, converts WETH into ETH before sending to "to" + /// @param convertToETH If true, converts WETH into ETH before sending to "to" /// @param calls The array of MultiCall structs encoding the operations to execute before liquidating the account. function liquidateCreditAccount( address creditAccount, address to, uint256 skipTokenMask, - bool convertWETH, + bool convertToETH, MultiCall[] calldata calls ) external payable override whenNotPausedOrEmergency nonZeroAddress(to) nonReentrant { // Checks that the CA exists to revert early for late liquidations and save gas @@ -373,10 +388,11 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { CollateralDebtData memory collateralDebtData; { ClaimAction claimAction; - (claimAction, closeAction, collateralDebtData) = + bool isLiquidatable; + (claimAction, closeAction, collateralDebtData, isLiquidatable) = _isAccountLiquidatable({creditAccount: creditAccount, isEmergency: paused()}); // F:[FA-14] - if (!collateralDebtData.isLiquidatable) revert CreditAccountNotLiquidatableException(); + if (!isLiquidatable) revert CreditAccountNotLiquidatableException(); collateralDebtData.enabledTokensMask = collateralDebtData.enabledTokensMask.enable( _claimWithdrawals({action: claimAction, creditAccount: creditAccount, to: borrower}) @@ -392,6 +408,10 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { collateralDebtData.enabledTokensMask = fullCheckParams.enabledTokensMaskAfter; } // F:[FA-15] + /// Bot permissions are specific to (owner, creditAccount), + /// so they need to be erased on account closure + _eraseAllBotPermissions({creditAccount: creditAccount, setFlag: false}); + (uint256 remainingFunds, uint256 reportedLoss) = _closeCreditAccount({ creditAccount: creditAccount, closureAction: closeAction, @@ -399,13 +419,17 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { payer: msg.sender, to: to, skipTokensMask: skipTokenMask, - convertWETH: convertWETH + convertToETH: convertToETH }); // F:[FA-15,49] + /// If there is non-zero loss, then borrowing is forbidden in + /// case this is an attack and there is risk of copycats afterwards + /// If cumulative loss exceeds maxCumulativeLoss, the CF is paused, + /// which ensures that the attacker can create at most maxCumulativeLoss + maxBorrowedAmount of bad debt if (reportedLoss > 0) { maxDebtPerBlockMultiplier = 0; // F: [FA-15A] - /// reportedLoss is always less uint128, because + /// reportedLoss is always less than uint128, because /// maxLoss = maxBorrowAmount which is uint128 lossParams.currentCumulativeLoss += uint128(reportedLoss); if (lossParams.currentCumulativeLoss > lossParams.maxCumulativeLoss) { @@ -414,14 +438,14 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { } // TODO: add test - if (convertWETH) { + if (convertToETH) { _wethWithdrawTo(to); } emit LiquidateCreditAccount(creditAccount, borrower, msg.sender, to, closeAction, remainingFunds); // F:[FA-15] } - /// @dev Executes a batch of transactions within a Multicall, to manage an existing account + /// @notice Executes a batch of transactions within a Multicall, to manage an existing account /// - Wraps ETH and sends it back to msg.sender, if value > 0 /// - Executes the Multicall /// - Performs a fullCollateralCheck to verify that hf > 1 after all actions @@ -441,9 +465,9 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { _multicallFullCollateralCheck(creditAccount, calls, ALL_PERMISSIONS); } - /// @dev Executes a batch of transactions within a Multicall from bot on behalf of a borrower - /// - Wraps ETH and sends it back to msg.sender, if value > 0 - /// - Executes the Multicall + /// @notice Executes a batch of transactions within a Multicall from bot on behalf of a Credit Account's owner + /// - Retrieves bot permissions from botList and checks whether it is forbidden + /// - Executes the Multicall, with actions limited to `botPermissions` /// - Performs a fullCollateralCheck to verify that hf > 1 after all actions /// @param creditAccount Address of credit account /// @param calls The array of MultiCall structs encoding the operations to execute. @@ -454,25 +478,30 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { whenNotExpired nonReentrant { - address borrower = _getBorrowerOrRevert(creditAccount); // F:[FA-2] - uint256 botPermissions = IBotList(botList).botPermissions(borrower, msg.sender); + (uint256 botPermissions, bool forbidden) = IBotList(botList).getBotStatus(creditAccount, msg.sender); // Checks that the bot is approved by the borrower and is not forbidden - if (botPermissions == 0 || IBotList(botList).forbiddenBot(msg.sender)) { + if (botPermissions == 0 || forbidden) { revert NotApprovedBotException(); // F: [FA-58] } - _multicallFullCollateralCheck(creditAccount, calls, botPermissions); + _multicallFullCollateralCheck(creditAccount, calls, botPermissions | PAY_BOT_PERMISSION); } + /// @notice Convenience internal function that packages a multicall and a fullCheck together, + /// since they one is always performed after the other (except for account opening/closing) function _multicallFullCollateralCheck(address creditAccount, MultiCall[] calldata calls, uint256 permissions) internal - nonZeroCallsOnly(calls) { + /// V3 checks forbidden tokens at the end of the multicall. Three conditions have to be fulfilled for + /// a multicall to be successful: + /// - No new forbidden tokens can be enabled during the multicall + /// - Forbidden token balances cannot be increased during the multicall + /// - Debt cannot be increased while forbidden tokens are enabled on an account + /// This ensures that no pool funds can be used to increase exposure to forbidden tokens. To that end, + /// before the multicall forbidden token balances are stored to compare with balances after uint256 _forbiddenTokenMask = forbiddenTokenMask; - - uint256 enabledTokensMaskBefore = creditManager.enabledTokensMaskOf(creditAccount); - - uint256[] memory forbiddenBalances = CreditLogic.storeForbiddenBalances({ + uint256 enabledTokensMaskBefore = ICreditManagerV3(creditManager).enabledTokensMaskOf(creditAccount); + uint256[] memory forbiddenBalances = BalancesLogic.storeForbiddenBalances({ creditAccount: creditAccount, forbiddenTokenMask: _forbiddenTokenMask, enabledTokensMask: enabledTokensMaskBefore, @@ -481,35 +510,43 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { FullCheckParams memory fullCheckParams = _multicall(creditAccount, calls, enabledTokensMaskBefore, permissions); - // Performs a fullCollateralCheck - // During a multicall, all intermediary health checks are skipped, - // as one fullCollateralCheck at the end is sufficient - _fullCollateralCheck( - creditAccount, enabledTokensMaskBefore, fullCheckParams, forbiddenBalances, _forbiddenTokenMask - ); + // Performs one fullCollateralCheck at the end of a multicall + _fullCollateralCheck({ + creditAccount: creditAccount, + enabledTokensMaskBefore: enabledTokensMaskBefore, + fullCheckParams: fullCheckParams, + forbiddenBalances: forbiddenBalances, + _forbiddenTokenMask: _forbiddenTokenMask + }); } - /// @dev IMPLEMENTATION: multicall - /// - Transfers ownership from borrower to this contract, as most adapter and Credit Manager functions retrieve - /// the Credit Account by msg.sender + /// @notice IMPLEMENTATION: multicall /// - Executes the provided list of calls: - /// + if targetContract == address(this), parses call data in the struct and calls the appropriate function (see _processCreditFacadeMulticall below) - /// + if targetContract == adapter, calls the adapter with call data as provided. Adapters skip health checks when Credit Facade is the msg.sender, - /// as it performs the necessary health checks on its own + /// + if targetContract == address(this), parses call data in the struct and calls the appropriate function + /// + if targetContract != address(this), checks that the address is an adapter and calls with calldata as provided. + /// - For all calls, there are usually additional check and actions performed (see each action below for more details) + /// @dev Unlike previous versions, in Gearbox V3 the mid-multicall enabledTokensMask is kept on the stack and updated based on values + /// returned from the Credit Manager and adapter functions. enabledTokensMask in storage is only updated once at the end of fullCollateralCheck. /// @param creditAccount Credit Account address - // / @param isClosure Whether the multicall is being invoked during a closure action. Calls to Credit Facade are forbidden inside - // / multicalls on closure. - // / @param increaseDebtWasCalled True if debt was increased before or during the multicall. Used to prevent free flashloans by - // / increasing and decreasing debt within a single multicall. - // fullCheckParams Parameters for the full collateral check which can be changed with a special function in a multicall - // - collateralHints: Array of token masks that determines the order in which tokens are checked, to optimize - // gas in the fullCollateralCheck cycle - // - minHealthFactor: A custom minimal HF threshold. Cannot be lower than PERCENTAGE_FACTOR + /// @param calls List of calls to perform + /// @param enabledTokensMask The mask of tokens enabled on the account before the multicall + /// @param flags A bit mask of flags that encodes permissions, as well as other important information + /// that needs to persist throughout the multicall + /// @return fullCheckParams Parameters passed to the full collateral check after the multicall + /// - collateralHints: Array of token masks that determines the order in which tokens are checked, to optimize + /// gas in the fullCollateralCheck cycle + /// - minHealthFactor: A custom minimal HF threshold. Cannot be lower than PERCENTAGE_FACTOR + /// - enabledTokensMaskAfter: The mask of tokens enabled on the account after the multicall + /// The enabledTokensMask value in Credit Manager storage is updated + /// during the fullCollateralCheck function _multicall(address creditAccount, MultiCall[] calldata calls, uint256 enabledTokensMask, uint256 flags) internal returns (FullCheckParams memory fullCheckParams) { - uint256 quotedTokenMaskInverted = ~creditManager.quotedTokenMask(); + /// Inverted mask of quoted tokens is pre-compute to avoid + /// enabling or disabling them outside `updateQuota` + uint256 quotedTokensMaskInverted = ~ICreditManagerV3(creditManager).quotedTokensMask(); + // Emits event for multicall start - used in analytics to track actions within multicalls emit StartMultiCall(creditAccount); // F:[FA-26] @@ -524,7 +561,7 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { unchecked { for (uint256 i = 0; i < len; ++i) { MultiCall calldata mcall = calls[i]; // F:[FA-26] - //xw + // // CREDIT FACADE // if (mcall.target == address(this)) { @@ -534,8 +571,11 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { bytes4 method = bytes4(mcall.callData); // - // REVERT_IF_RECEIVED_LESS_THAN + // REVERT IF RECEIVED LESS THAN // + /// Method allows the user to enable slippage control, verifying that + /// the multicall has produced expected minimal token balances + /// Used as protection against sandwiching and untrusted path providers if (method == ICreditFacadeMulticall.revertIfReceivedLessThan.selector) { // Method can only be called once since the provided Balance array // contains deltas that are added to the current balances @@ -546,35 +586,53 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { } // F:[FA-45A] // Sets expected balances to currentBalance + delta - expectedBalances = CreditLogic.storeBalances(creditAccount, mcall.callData[4:]); // F:[FA-45] + Balance[] memory expected = abi.decode(mcall.callData[4:], (Balance[])); // F:[FA-45] + expectedBalances = BalancesLogic.storeBalances(creditAccount, expected); // F:[FA-45] } // // SET FULL CHECK PARAMS // + /// Sets the parameters to be used during the full collateral check. + /// Collateral hints can be used to check tokens in a particular order - this allows + /// to put the most valuable tokens first and save gas, as full collateral check eval + /// is lazy. minHealthFactor can be used to set a custom health factor threshold, which + /// is especially useful for bots. else if (method == ICreditFacadeMulticall.setFullCheckParams.selector) { (fullCheckParams.collateralHints, fullCheckParams.minHealthFactor) = abi.decode(mcall.callData[4:], (uint256[], uint16)); } // + // ON DEMAND PRICE UPDATE + // + /// Utility function that enables support for price feeds with on-demand + /// price updates. This helps support tokens where there is no traditional price feeds, + /// but there is attested off-chain price data. + else if (method == ICreditFacadeMulticall.onDemandPriceUpdate.selector) { + _onDemandPriceUpdate(mcall.callData[4:]); + } + // // ADD COLLATERAL // + /// Transfers new collateral from the caller to the Credit Account. else if (method == ICreditFacadeMulticall.addCollateral.selector) { _revertIfNoPermission(flags, ADD_COLLATERAL_PERMISSION); enabledTokensMask = enabledTokensMask.enable({ bitsToEnable: _addCollateral(creditAccount, mcall.callData[4:]), - invertedSkipMask: quotedTokenMaskInverted + invertedSkipMask: quotedTokensMaskInverted }); // F:[FA-26, 27] } // // INCREASE DEBT // + /// Increases the Credit Account's debt and sends the new borrowed funds + /// from the pool to the Credit Account. Changes some flags, + /// in order to enforce some restrictions after increasing debt, + /// such is decreaseDebt or having forbidden tokens being prohibited else if (method == ICreditFacadeMulticall.increaseDebt.selector) { _revertIfNoPermission(flags, INCREASE_DEBT_PERMISSION); - // Sets increaseDebtWasCalled to prevent debt reductions afterwards, - // as that could be used to get free flash loans - flags &= ~DECREASE_DEBT_PERMISSION; // F:[FA-28] - flags |= INCREASE_DEBT_WAS_CALLED; + flags = flags.enable(INCREASE_DEBT_WAS_CALLED).disable(DECREASE_DEBT_PERMISSION); // F:[FA-28] + (uint256 tokensToEnable, uint256 tokensToDisable) = _manageDebt( creditAccount, mcall.callData[4:], enabledTokensMask, ManageDebtAction.INCREASE_DEBT ); // F:[FA-26] @@ -583,6 +641,7 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { // // DECREASE DEBT // + /// Decreases the Credit Account's debt and sends the funds back to the pool else if (method == ICreditFacadeMulticall.decreaseDebt.selector) { // it's forbidden to call decreaseDebt after increaseDebt, in the same multicall _revertIfNoPermission(flags, DECREASE_DEBT_PERMISSION); @@ -596,18 +655,22 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { // // ENABLE TOKEN // + /// Enables a token on a Credit Account, which includes it into collateral + /// computations else if (method == ICreditFacadeMulticall.enableToken.selector) { _revertIfNoPermission(flags, ENABLE_TOKEN_PERMISSION); // Parses token address token = abi.decode(mcall.callData[4:], (address)); // F: [FA-53] enabledTokensMask = enabledTokensMask.enable({ bitsToEnable: _getTokenMaskOrRevert(token), - invertedSkipMask: quotedTokenMaskInverted + invertedSkipMask: quotedTokensMaskInverted }); } // // DISABLE TOKEN // + /// Disables a token on a Credit Account, which excludes it from collateral + /// computations else if (method == ICreditFacadeMulticall.disableToken.selector) { _revertIfNoPermission(flags, DISABLE_TOKEN_PERMISSION); // Parses token @@ -615,38 +678,57 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { /// IGNORE QUOTED TOKEN MASK enabledTokensMask = enabledTokensMask.disable({ bitsToDisable: _getTokenMaskOrRevert(token), - invertedSkipMask: quotedTokenMaskInverted + invertedSkipMask: quotedTokensMaskInverted }); } // // UPDATE QUOTA // + /// Updates a quota on a token. Quota is an underlying-denominated value + /// that imposes a limit on the exposure of borrowed funds to a certain asset. + /// Tokens with quota logic are only enabled and disabled on updating the quota + /// from zero to positive value and back, respectively. else if (method == ICreditFacadeMulticall.updateQuota.selector) { _revertIfNoPermission(flags, UPDATE_QUOTA_PERMISSION); (uint256 tokensToEnable, uint256 tokensToDisable) = - _updateQuota(creditAccount, mcall.callData[4:], enabledTokensMask); + _updateQuota(creditAccount, mcall.callData[4:]); enabledTokensMask = enabledTokensMask.enableDisable(tokensToEnable, tokensToDisable); } // // WITHDRAW // + /// Schedules a delayed withdrawal of assets from a Credit Account. + /// This sends asset from the CA to the withdrawal manager and excludes them + /// from collateral computations (with some exceptions). After a delay, + /// the account owner can claim the withdrawal. else if (method == ICreditFacadeMulticall.scheduleWithdrawal.selector) { _revertIfNoPermission(flags, WITHDRAW_PERMISSION); uint256 tokensToDisable = _scheduleWithdrawal(creditAccount, mcall.callData[4:]); - /// IGNORE QUOTED TOKEN MASK enabledTokensMask = enabledTokensMask.disable({ bitsToDisable: tokensToDisable, - invertedSkipMask: quotedTokenMaskInverted + invertedSkipMask: quotedTokensMaskInverted }); } // - // RevokeAdapterAllowances + // REVOKE ADAPTER ALLOWANCES // + /// Sets allowance to the provided list of contracts to one. Can be used + /// to clean up leftover allowances from old contracts else if (method == ICreditFacadeMulticall.revokeAdapterAllowances.selector) { _revertIfNoPermission(flags, REVOKE_ALLOWANCES_PERMISSION); _revokeAdapterAllowances(creditAccount, mcall.callData[4:]); } // + // PAY BOT + // + /// Requests the bot list to pay a bot. Used by bots to receive payment for their services. + /// Only available in `botMulticall` and can only be called once + else if (method == ICreditFacadeMulticall.payBot.selector) { + _revertIfNoPermission(flags, PAY_BOT_PERMISSION); + flags = flags.disable(PAY_BOT_PERMISSION); + _payBot(creditAccount, mcall.callData[4:]); + } + // // UNKNOWN METHOD // else { @@ -657,28 +739,30 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { // ADAPTERS // _revertIfNoPermission(flags, EXTERNAL_CALLS_PERMISSION); - // Checks that the target is an allowed adapter and not CreditManagerV3 - // As CreditFacadeV3 has powerful permissions in CreditManagers, - // functionCall to it is strictly forbidden, even if - // the Configurator adds it as an adapter - if (creditManager.adapterToContract(mcall.target) == address(0)) { + // Checks that the target is an allowed adapter in Credit Manager + if (ICreditManagerV3(creditManager).adapterToContract(mcall.target) == address(0)) { revert TargetContractNotAllowedException(); } // F:[FA-24] + /// The `externalCallCreditAccount` value in CreditManager is set to the currently processed + /// Credit Account. This value is used by adapters to retrieve the CA that is being worked on + /// After the multicall, the value is set back to address(1) if (flags & EXTERNAL_CONTRACT_WAS_CALLED == 0) { - flags |= EXTERNAL_CONTRACT_WAS_CALLED; - _setCaForExterallCall(creditAccount); + flags = flags.enable(EXTERNAL_CONTRACT_WAS_CALLED); + _setActiveCreditAccount(creditAccount); } - // Makes a call + /// Performs an adapter call. Each external adapter function returns + /// the masks of tokens to enable and disable, which are applied to the mask + /// on the stack; the net change in the enabled token set is saved to storage + /// only in fullCollateralCheck at the end of the multicall bytes memory result = mcall.target.functionCall(mcall.callData); // F:[FA-29] (uint256 tokensToEnable, uint256 tokensToDisable) = abi.decode(result, (uint256, uint256)); - /// IGNORE QUOTED TOKEN MASK enabledTokensMask = enabledTokensMask.enableDisable({ bitsToEnable: tokensToEnable, bitsToDisable: tokensToDisable, - invertedSkipMask: quotedTokenMaskInverted + invertedSkipMask: quotedTokensMaskInverted }); } } @@ -687,76 +771,98 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { // If expectedBalances was set by calling revertIfGetLessThan, // checks that actual token balances are not less than expected balances if (expectedBalances.length != 0) { - CreditLogic.compareBalances(creditAccount, expectedBalances); + BalancesLogic.compareBalances(creditAccount, expectedBalances); } - /// @dev Checks that there are no intersections between the user's enabled tokens - /// and the set of forbidden tokens - /// @notice The main purpose of forbidding tokens is to prevent exposing - /// pool funds to dangerous or exploited collateral, without immediately - /// liquidating accounts that hold the forbidden token - /// There are two ways pool funds can be exposed: - /// - The CA owner tries to swap borrowed funds to the forbidden asset: - /// this will be blocked by checkAndEnableToken, which is invoked for tokenOut - /// after every operation; - /// - The CA owner with an already enabled forbidden token transfers it - /// to the account - they can't use addCollateral / enableToken due to checkAndEnableToken, - /// but can transfer the token directly when it is enabled and it will be counted in the collateral - - /// an borrows against it. This check is used to prevent this. - /// If the owner has a forbidden token and want to take more debt, they must first - /// dispose of the token and disable it. - if ((flags & INCREASE_DEBT_WAS_CALLED != 0) && (enabledTokensMask & forbiddenTokenMask > 0)) { + /// If increaseDebt was called during the multicall, all forbidden tokens must be disabled at the end + /// otherwise, funds could be borrowed against forbidden token, which is prohibited + if ((flags & INCREASE_DEBT_WAS_CALLED != 0) && (enabledTokensMask & forbiddenTokenMask != 0)) { revert ForbiddenTokensException(); } + /// If the `externalCallCreditAccount` value was set to the current CA, it must be reset if (flags & EXTERNAL_CONTRACT_WAS_CALLED != 0) { - _returnCaForExterallCall(); + _unsetActiveCreditAccount(); } - // Emits event for multicall end - used in analytics to track actions within multicalls - // Emits event for multicall start - used in analytics to track actions within multicalls + /// Emits event for multicall end - used in analytics to track actions within multicalls emit FinishMultiCall(); // F:[FA-27,27,29] + /// Saves the final enabledTokensMask to be later passed into the fullCollateralCheck, + /// where it will be saved to storage fullCheckParams.enabledTokensMaskAfter = enabledTokensMask; } - function _setCaForExterallCall(address creditAccount) internal { - // Takes ownership of the Credit Account - _setExternalCallCreditAccount(creditAccount); // F:[FA-26] - } - - function _returnCaForExterallCall() internal { - // Takes ownership of the Credit Account - _setExternalCallCreditAccount(address(1)); // F:[FA-26] + /// @notice Sets the `activeCreditAccount` in Credit Manager + /// to the passed Credit Account + /// @param creditAccount CA address + function _setActiveCreditAccount(address creditAccount) internal { + ICreditManagerV3(creditManager).setActiveCreditAccount(creditAccount); // F:[FA-26] } - function _setExternalCallCreditAccount(address creditAccount) internal { - creditManager.setCreditAccountForExternalCall(creditAccount); // F:[FA-26] + /// @notice Sets the `externalCallCreditAccount` in Credit Manager + /// to the default value + function _unsetActiveCreditAccount() internal { + _setActiveCreditAccount(address(1)); // F:[FA-26] } + /// @notice Reverts if provided flags contain no permission for the requested action + /// @param flags A bitmask with flags for the multicall operation + /// @param permission The flag of the permission to check function _revertIfNoPermission(uint256 flags, uint256 permission) internal pure { if (flags & permission == 0) { revert NoPermissionException(permission); } } - function _updateQuota(address creditAccount, bytes calldata callData, uint256 enabledTokensMask) + /// @notice Requests an on-demand price update from a price feed + /// The price update accepts a generic data blob that is processed + /// on the price feed side. + /// @dev Should generally be called only when interacting with tokens + /// that use on-demand price feeds + /// @param callData Bytes calldata for parsing + function _onDemandPriceUpdate(bytes calldata callData) internal { + (address token, bytes memory data) = abi.decode(callData, (address, bytes)); + + address priceFeed = IPriceOracleV2(ICreditManagerV3(creditManager).priceOracle()).priceFeeds(token); + if (priceFeed == address(0)) revert PriceFeedNotExistsException(); + + IPriceFeedOnDemand(priceFeed).updatePrice(data); + } + + /// @notice Requests Credit Manager to update a Credit Account's quota for a certain token + /// @param creditAccount Credit Account to update the quota for + /// @param callData Bytes calldata for parsing + function _updateQuota(address creditAccount, bytes calldata callData) internal returns (uint256 tokensToEnable, uint256 tokensToDisable) { (address token, int96 quotaChange) = abi.decode(callData, (address, int96)); - return creditManager.updateQuota(creditAccount, token, quotaChange); + return ICreditManagerV3(creditManager).updateQuota(creditAccount, token, quotaChange); } + /// @notice Requests Credit Manager to remove a set of existing allowances + /// @param creditAccount Credit Account to revoke allowances for + /// @param callData Bytes calldata for parsing function _revokeAdapterAllowances(address creditAccount, bytes calldata callData) internal { (RevocationPair[] memory revocations) = abi.decode(callData, (RevocationPair[])); - creditManager.revokeAdapterAllowances(creditAccount, revocations); + ICreditManagerV3(creditManager).revokeAdapterAllowances(creditAccount, revocations); } - /// @dev Adds expected deltas to current balances on a Credit account and returns the result + /// @notice Requests the bot list to pay the bot for performed services + /// @param creditAccount Credit account the service was performed for + /// @param callData Bytes calldata for parsing + function _payBot(address creditAccount, bytes calldata callData) internal { + uint72 paymentAmount = abi.decode(callData, (uint72)); + + /// The current owner of the account always pays for bot services + address payer = _getBorrowerOrRevert(creditAccount); - /// @dev Increases debt for a Credit Account - /// @param creditAccount CA to increase debt for + IBotList(botList).payBot(payer, creditAccount, msg.sender, paymentAmount); + } + + /// @notice Requests the Credit Manager to change the CA's debt + /// @param creditAccount CA to change debt for /// @param callData Bytes calldata for parsing function _manageDebt( address creditAccount, @@ -768,13 +874,15 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { if (action == ManageDebtAction.INCREASE_DEBT) { // Checks that the borrowed amount does not violate the per block limit - _checkIncreaseDebtAllowedAndUpdateBlockLimit(amount); // F:[FA-18A] + // This also ensures that increaseDebt can't be called when borrowing is forbidden + // (since the block limit will be 0) + _revertIfOutOfBorrowingLimit(amount); // F:[FA-18A] } uint256 newDebt; // Requests the Credit Manager to borrow additional funds from the pool (newDebt, tokensToEnable, tokensToDisable) = - creditManager.manageDebt(creditAccount, amount, enabledTokensMask, action); // F:[FA-17] + ICreditManagerV3(creditManager).manageDebt(creditAccount, amount, enabledTokensMask, action); // F:[FA-17] // Checks that the new total borrowed amount is within bounds _revertIfOutOfDebtLimits(newDebt); // F:[FA-18B] @@ -787,27 +895,34 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { } } + /// @notice Requests the Credit Manager to transfer collateral from the caller to the Credit Account + /// @param creditAccount Credit Account to add collateral for + /// @param callData Bytes calldata for parsing function _addCollateral(address creditAccount, bytes calldata callData) internal returns (uint256 tokenMaskAfter) { (address token, uint256 amount) = abi.decode(callData, (address, uint256)); // F:[FA-26, 27] // Requests Credit Manager to transfer collateral to the Credit Account - tokenMaskAfter = creditManager.addCollateral(msg.sender, creditAccount, token, amount); // F:[FA-21] + tokenMaskAfter = ICreditManagerV3(creditManager).addCollateral(msg.sender, creditAccount, token, amount); // F:[FA-21] // Emits event emit AddCollateral(creditAccount, token, amount); // F:[FA-21] } + /// @notice Requests the Credit Manager to schedule a withdrawal + /// @param creditAccount Credit Account to schedule withdrawals for + /// @param callData Bytes calldata for parsing function _scheduleWithdrawal(address creditAccount, bytes calldata callData) internal returns (uint256 tokensToDisable) { (address token, uint256 amount) = abi.decode(callData, (address, uint256)); - tokensToDisable = creditManager.scheduleWithdrawal(creditAccount, token, amount); + tokensToDisable = ICreditManagerV3(creditManager).scheduleWithdrawal(creditAccount, token, amount); } - /// @dev Transfers credit account to another user + /// @notice Transfers credit account from owner to another user /// By default, this action is forbidden, and the user has to approve transfers from sender to itself /// by calling approveAccountTransfer. /// This is done to prevent malicious actors from transferring compromised accounts to other users. + /// @param creditAccount Address of CA to transfer /// @param to Address to transfer the account to function transferAccountOwnership(address creditAccount, address to) external @@ -818,21 +933,29 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { creditAccountOwnerOnly(creditAccount) nonReentrant { - _revertIfAccountTransferNotAllowed(msg.sender, to); + _revertIfAccountTransferNotAllowed({from: msg.sender, to: to}); /// Checks that the account hf > 1, as it is forbidden to transfer /// accounts that are liquidatable - (,, CollateralDebtData memory collateralDebtData) = _isAccountLiquidatable(creditAccount, false); // F:[FA-34] + (,,, bool isLiquidatable) = _isAccountLiquidatable(creditAccount, false); // F:[FA-34] - if (collateralDebtData.isLiquidatable) revert CantTransferLiquidatableAccountException(); // F:[FA-34] + if (isLiquidatable) revert CantTransferLiquidatableAccountException(); // F:[FA-34] + + /// Bot permissions are specific to (owner, creditAccount), + /// so they need to be erased on account transfer + _eraseAllBotPermissions({creditAccount: creditAccount, setFlag: true}); // Requests the Credit Manager to transfer the account - creditManager.transferAccountOwnership(creditAccount, to); // F:[FA-35] + ICreditManagerV3(creditManager).transferAccountOwnership(creditAccount, to); // F:[FA-35] // Emits event emit TransferAccount(creditAccount, msg.sender, to); // F:[FA-35] } + /// @notice Claims all mature delayed withdrawals, transferring funds from + /// withdrawal manager to the address provided by the CA owner + /// @param creditAccount CA to claim withdrawals for + /// @param to Address to transfer the withdrawals to function claimWithdrawals(address creditAccount, address to) external override @@ -844,16 +967,64 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { _claimWithdrawals(creditAccount, to, ClaimAction.CLAIM); } - /// @dev Checks that transfer is allowed + /// @notice Sets permissions and funding parameters for a bot + /// Also manages BOT_PERMISSIONS_SET_FLAG, to allow + /// the contracts to determine whether a CA has permissions for any bot + /// @param creditAccount CA to set permissions for + /// @param bot Bot to set permissions for + /// @param permissions A bit mask of permissions + /// @param fundingAmount Total amount of ETH available to the bot for payments + /// @param weeklyFundingAllowance Amount of ETH available to the bot weekly + function setBotPermissions( + address creditAccount, + address bot, + uint192 permissions, + uint72 fundingAmount, + uint72 weeklyFundingAllowance + ) external override whenNotPaused creditAccountOwnerOnly(creditAccount) nonReentrant { + uint16 flags = ICreditManagerV3(creditManager).flagsOf(creditAccount); + + if (flags & BOT_PERMISSIONS_SET_FLAG == 0) { + _eraseAllBotPermissions({creditAccount: creditAccount, setFlag: false}); + + if (permissions != 0) { + _setFlagFor(creditAccount, BOT_PERMISSIONS_SET_FLAG, true); + } + } + + uint256 remainingBots = + IBotList(botList).setBotPermissions(creditAccount, bot, permissions, fundingAmount, weeklyFundingAllowance); + if (remainingBots == 0) { + _setFlagFor(creditAccount, BOT_PERMISSIONS_SET_FLAG, false); + } + } + + /// @notice Convenience function to erase all bot permissions for a Credit Account + /// Called on transferring or closing an account + function _eraseAllBotPermissions(address creditAccount, bool setFlag) internal { + IBotList(botList).eraseAllBotPermissions(creditAccount); + + if (setFlag) { + _setFlagFor(creditAccount, BOT_PERMISSIONS_SET_FLAG, false); + } + } + + /// @notice Internal wrapper for `CreditManager.setFlagFor()`. The external call is wrapped + /// to optimize contract size + function _setFlagFor(address creditAccount, uint16 flag, bool value) internal { + ICreditManagerV3(creditManager).setFlagFor(creditAccount, flag, value); + } + + /// @notice Checks that transfer is allowed function _revertIfAccountTransferNotAllowed(address from, address to) internal view { if (!transfersAllowed[from][to]) { revert AccountTransferNotAllowedException(); } // F:[FA-33] } - /// @dev Checks that the per-block borrow limit was not violated and updates the + /// @notice Checks that the per-block borrow limit was not violated and updates the /// amount borrowed in current block - function _checkIncreaseDebtAllowedAndUpdateBlockLimit(uint256 amount) internal { + function _revertIfOutOfBorrowingLimit(uint256 amount) internal { uint8 _maxDebtPerBlockMultiplier = maxDebtPerBlockMultiplier; // F:[FA-18]\ if (_maxDebtPerBlockMultiplier == 0) { @@ -878,7 +1049,7 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { totalBorrowedInBlock = uint128(newDebtInCurrentBlock); } - /// @dev Checks that the borrowed principal is within borrowing debtLimits + /// @notice Checks that the borrowed principal is within borrowing debtLimits /// @param debt The current principal of a Credit Account function _revertIfOutOfDebtLimits(uint256 debt) internal view { // Checks that amount is in debtLimits @@ -887,7 +1058,7 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { } // F: } - /// @dev Approves account transfer from another user to msg.sender + /// @notice Approves account transfer from another user to msg.sender /// @param from Address for which account transfers are allowed/forbidden /// @param allowTransfer True is transfer is allowed, false if forbidden function approveAccountTransfer(address from, bool allowTransfer) external override nonReentrant { @@ -905,16 +1076,20 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { // HELPERS // - /// @dev Internal wrapper for `creditManager.getBorrowerOrRevert()` - /// @notice The external call is wrapped to optimize contract size - function _getBorrowerOrRevert(address borrower) internal view returns (address) { - return creditManager.getBorrowerOrRevert(borrower); + /// @notice Internal wrapper for `creditManager.getBorrowerOrRevert()` + /// @dev The external call is wrapped to optimize contract size + function _getBorrowerOrRevert(address creditAccount) internal view returns (address) { + return ICreditManagerV3(creditManager).getBorrowerOrRevert({creditAccount: creditAccount}); } + /// @notice Internal wrapper for `creditManager.getTokenMaskOrRevert()` + /// @dev The external call is wrapped to optimize contract size function _getTokenMaskOrRevert(address token) internal view returns (uint256 mask) { - mask = creditManager.getTokenMaskOrRevert(token); + mask = ICreditManagerV3(creditManager).getTokenMaskOrRevert(token); } + /// @notice Internal wrapper for `creditManager.closeCreditAccount()` + /// @dev The external call is wrapped to optimize contract size function _closeCreditAccount( address creditAccount, ClosureAction closureAction, @@ -922,21 +1097,21 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { address payer, address to, uint256 skipTokensMask, - bool convertWETH + bool convertToETH ) internal returns (uint256 remainingFunds, uint256 reportedLoss) { - (remainingFunds, reportedLoss) = creditManager.closeCreditAccount({ + (remainingFunds, reportedLoss) = ICreditManagerV3(creditManager).closeCreditAccount({ creditAccount: creditAccount, closureAction: closureAction, collateralDebtData: collateralDebtData, payer: payer, to: to, skipTokensMask: skipTokensMask, - convertWETH: convertWETH + convertToETH: convertToETH }); // F:[FA-15,49] } - /// @dev Internal wrapper for `creditManager.fullCollateralCheck()` - /// @notice The external call is wrapped to optimize contract size + /// @notice Internal wrapper for `creditManager.fullCollateralCheck()` + /// @dev The external call is wrapped to optimize contract size function _fullCollateralCheck( address creditAccount, uint256 enabledTokensMaskBefore, @@ -944,14 +1119,14 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { uint256[] memory forbiddenBalances, uint256 _forbiddenTokenMask ) internal { - creditManager.fullCollateralCheck( + ICreditManagerV3(creditManager).fullCollateralCheck( creditAccount, fullCheckParams.enabledTokensMaskAfter, fullCheckParams.collateralHints, fullCheckParams.minHealthFactor ); - CreditLogic.checkForbiddenBalances({ + BalancesLogic.checkForbiddenBalances({ creditAccount: creditAccount, enabledTokensMaskBefore: enabledTokensMaskBefore, enabledTokensMaskAfter: fullCheckParams.enabledTokensMaskAfter, @@ -961,30 +1136,31 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { }); } - /// @dev Returns whether the Credit Facade is expired + /// @notice Returns whether the Credit Facade is expired function _isExpired() internal view returns (bool isExpired) { isExpired = (expirable) && (block.timestamp >= expirationDate); // F: [FA-46,47,48] } - // - // GETTERS - // - - /// @dev Wraps ETH into WETH and sends it back to msg.sender + /// @notice Wraps ETH into WETH and sends it back to msg.sender /// TODO: Check L2 networks for supporting native currencies function _wrapETH() internal { if (msg.value > 0) { - IWETH(wethAddress).deposit{value: msg.value}(); // F:[FA-3] - IWETH(wethAddress).transfer(msg.sender, msg.value); // F:[FA-3] + IWETH(weth).deposit{value: msg.value}(); // F:[FA-3] + IWETH(weth).transfer(msg.sender, msg.value); // F:[FA-3] } } - /// @dev Checks if account is liquidatable (i.e., hf < 1) + /// @notice Checks if account is liquidatable (i.e., hf < 1) /// @param creditAccount Address of credit account to check function _isAccountLiquidatable(address creditAccount, bool isEmergency) internal view - returns (ClaimAction claimAction, ClosureAction closeAction, CollateralDebtData memory collateralDebtData) + returns ( + ClaimAction claimAction, + ClosureAction closeAction, + CollateralDebtData memory collateralDebtData, + bool isLiquidatable + ) { claimAction = isEmergency ? ClaimAction.FORCE_CANCEL : ClaimAction.CANCEL; collateralDebtData = _calcDebtAndCollateral( @@ -995,41 +1171,52 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { ); closeAction = ClosureAction.LIQUIDATE_ACCOUNT; - if (!collateralDebtData.isLiquidatable && _isExpired()) { - collateralDebtData.isLiquidatable = true; + isLiquidatable = collateralDebtData.twvUSD < collateralDebtData.totalDebtUSD; + + if (!isLiquidatable && _isExpired()) { + isLiquidatable = true; closeAction = ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT; } } + /// @notice Internal wrapper for `creditManager.calcDebtAndCollateral()` + /// @dev The external call is wrapped to optimize contract size function _calcDebtAndCollateral(address creditAccount, CollateralCalcTask task) internal view returns (CollateralDebtData memory) { - return creditManager.calcDebtAndCollateral(creditAccount, task); + return ICreditManagerV3(creditManager).calcDebtAndCollateral(creditAccount, task); } + /// @notice Internal wrapper for `creditManager.getTokenByMask()` + /// @dev The external call is wrapped to optimize contract size function _getTokenByMask(uint256 mask) internal view returns (address) { - return creditManager.getTokenByMask(mask); + return ICreditManagerV3(creditManager).getTokenByMask(mask); } + /// @notice Internal wrapper for `creditManager.claimWithdrawals()` + /// @dev The external call is wrapped to optimize contract size function _claimWithdrawals(address creditAccount, address to, ClaimAction action) internal returns (uint256 tokensToEnable) { - tokensToEnable = creditManager.claimWithdrawals(creditAccount, to, action); + tokensToEnable = ICreditManagerV3(creditManager).claimWithdrawals(creditAccount, to, action); } + /// @notice Internal wrapper for `IWETHGateway.withdrawTo()` + /// @dev The external call is wrapped to optimize contract size + /// @dev Used to convert WETH to ETH and send it to user function _wethWithdrawTo(address to) internal { - wethGateway.withdrawTo(to); + IWETHGateway(wethGateway).withdrawTo(to); } // // CONFIGURATION // - /// @dev Sets Credit Facade expiration date - /// @notice See more at https://dev.gearbox.fi/docs/documentation/credit/liquidation#liquidating-accounts-by-expiration + /// @notice Sets Credit Facade expiration date + /// @dev See more at https://dev.gearbox.fi/docs/documentation/credit/liquidation#liquidating-accounts-by-expiration function setExpirationDate(uint40 newExpirationDate) external creditConfiguratorOnly { if (!expirable) { revert NotAllowedWhenNotExpirableException(); @@ -1037,7 +1224,7 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { expirationDate = newExpirationDate; } - /// @dev Sets borrowing debtLimits per single Credit Account + /// @notice Sets borrowing debtLimits per single Credit Account /// @param _minDebt The minimal borrowed amount per Credit Account. Minimal amount can be relevant /// for liquidations, since very small amounts will make liquidations unprofitable for liquidators /// @param _maxDebt The maximal borrowed amount per Credit Account. Used to limit exposure per a single @@ -1051,7 +1238,7 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { maxDebtPerBlockMultiplier = _maxDebtPerBlockMultiplier; } - /// @dev Sets the bot list for this Credit Facade + /// @notice Sets the bot list for this Credit Facade /// The bot list is used to determine whether an address has a right to /// run multicalls for a borrower as a bot. The relationship is stored in a separate /// contract for easier transferability @@ -1059,7 +1246,9 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { botList = _botList; } - /// @dev Sets the max cumulative loss that can be accrued before pausing the Credit Manager + /// @notice Sets the max cumulative loss that can be accrued before pausing the Credit Manager + /// @param _maxCumulativeLoss The threshold of cumulative loss that triggers a system pause + /// @param resetCumulativeLoss Whether to reset the current cumulative loss function setCumulativeLossParams(uint128 _maxCumulativeLoss, bool resetCumulativeLoss) external creditConfiguratorOnly @@ -1070,21 +1259,20 @@ contract CreditFacadeV3 is ICreditFacade, ACLNonReentrantTrait { } } - /// @dev Adds forbidden token + /// @notice Changes the token's forbidden status + /// @param token Address of the token to set status for + /// @param allowance Status to set (ALLOW / FORBID) function setTokenAllowance(address token, AllowanceAction allowance) external creditConfiguratorOnly { uint256 tokenMask = _getTokenMaskOrRevert(token); - if (allowance == AllowanceAction.ALLOW) { - if (forbiddenTokenMask & tokenMask != 0) { - forbiddenTokenMask ^= tokenMask; - } - } else { - forbiddenTokenMask |= tokenMask; - } + forbiddenTokenMask = (allowance == AllowanceAction.ALLOW) + ? forbiddenTokenMask.disable(tokenMask) + : forbiddenTokenMask.enable(tokenMask); } - /// @dev Adds an address to the list of emergency liquidators - /// @param liquidator Address to add to the list + /// @notice Changes the status of an emergency liquidator + /// @param liquidator Address to change status for + /// @param allowanceAction Status to set (ALLOW / FORBID) function setEmergencyLiquidator(address liquidator, AllowanceAction allowanceAction) external creditConfiguratorOnly // F:[CM-4] diff --git a/contracts/credit/CreditManagerV3.sol b/contracts/credit/CreditManagerV3.sol index 8aba5bcc..9dd4a21e 100644 --- a/contracts/credit/CreditManagerV3.sol +++ b/contracts/credit/CreditManagerV3.sol @@ -4,25 +4,25 @@ pragma solidity ^0.8.17; // LIBRARIES -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; // LIBS & TRAITS import {UNDERLYING_TOKEN_MASK, BitMask} from "../libraries/BitMask.sol"; import {CreditLogic} from "../libraries/CreditLogic.sol"; +import {CollateralLogic} from "../libraries/CollateralLogic.sol"; import {CreditAccountHelper} from "../libraries/CreditAccountHelper.sol"; -import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {ReentrancyGuardTrait} from "../traits/ReentrancyGuardTrait.sol"; import {SanityCheckTrait} from "../traits/SanityCheckTrait.sol"; import {IERC20Helper} from "../libraries/IERC20Helper.sol"; // INTERFACES -import {IAccountFactory, TakeAccountAction} from "../interfaces/IAccountFactory.sol"; +import {IAccountFactory} from "../interfaces/IAccountFactory.sol"; import {ICreditAccount} from "../interfaces/ICreditAccount.sol"; -import {IPoolService} from "@gearbox-protocol/core-v2/contracts/interfaces/IPoolService.sol"; -import {IPool4626} from "../interfaces/IPool4626.sol"; +import {IPoolBase, IPoolV3} from "../interfaces/IPoolV3.sol"; import {IWETHGateway} from "../interfaces/IWETHGateway.sol"; import {ClaimAction, IWithdrawalManager} from "../interfaces/IWithdrawalManager.sol"; import { @@ -36,7 +36,7 @@ import { CollateralCalcTask, WITHDRAWAL_FLAG } from "../interfaces/ICreditManagerV3.sol"; -import {IAddressProvider} from "@gearbox-protocol/core-v2/contracts/interfaces/IAddressProvider.sol"; +import "../interfaces/IAddressProviderV3.sol"; import {IPriceOracleV2} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; import {IPoolQuotaKeeper} from "../interfaces/IPoolQuotaKeeper.sol"; import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; @@ -53,230 +53,239 @@ import { // EXCEPTIONS import "../interfaces/IExceptions.sol"; -import "forge-std/console.sol"; - /// @title Credit Manager -/// @notice Encapsulates the business logic for managing Credit Accounts +/// @dev Encapsulates the business logic for managing Credit Accounts /// /// More info: https://dev.gearbox.fi/developers/credit/credit_manager -contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard { +contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardTrait { using EnumerableSet for EnumerableSet.AddressSet; - using Address for address payable; using BitMask for uint256; - using CreditLogic for CollateralDebtData; using CreditLogic for CollateralTokenData; + using CreditLogic for CollateralDebtData; + using CollateralLogic for CollateralDebtData; using SafeERC20 for IERC20; using IERC20Helper for IERC20; using CreditAccountHelper for ICreditAccount; // IMMUTABLE PARAMS - /// @dev contract version + + /// @inheritdoc IVersion uint256 public constant override version = 3_00; - /// @dev Factory contract for Credit Accounts - IAccountFactory public immutable accountFactory; + /// @notice Address provider + /// @dev While not used in this contract outside the constructor, + /// it is routinely used by other connected contracts + address public immutable override addressProvider; - /// @dev Address of the underlying asset + /// @notice Factory contract for Credit Accounts + address public immutable accountFactory; + + /// @notice Address of the underlying asset address public immutable override underlying; - /// @dev Address of the connected pool + /// @notice Address of the connected pool address public immutable override pool; - /// @dev Address of WETH - address public immutable override wethAddress; + /// @notice Address of WETH + address public immutable override weth; - /// @dev Address of WETH Gateway + /// @notice Address of WETH Gateway address public immutable wethGateway; - /// @dev Whether the CM supports quota-related logic + /// @notice Whether the CM supports quota-related logic bool public immutable override supportsQuotas; - uint256 private immutable deployAccountAction; - - /// @dev The maximal number of enabled tokens on a single Credit Account - uint8 public override maxAllowedEnabledTokenLength = 12; - - /// @dev Address of the connected Credit Facade + /// @notice Address of the connected Credit Facade address public override creditFacade; - /// @dev Points to creditAccount during multicall, otherwise keeps address(1) for gas savings - /// CreditFacade is trusted source, so primarly it sends creditAccount as parameter - /// _externalCallCreditAccount is used for adapters interation when adapter calls approve / execute methods - address internal _externalCallCreditAccount; + /// @notice The maximal number of enabled tokens on a single Credit Account + uint8 public override maxEnabledTokens = 12; - /// @dev Interest fee charged by the protocol: fee = interest accrued * feeInterest + /// @notice Liquidation threshold for the underlying token. + uint16 internal ltUnderlying; + + /// @notice Interest fee charged by the protocol: fee = interest accrued * feeInterest (this includes quota interest) uint16 internal feeInterest; - /// @dev Liquidation fee charged by the protocol: fee = totalValue * feeLiquidation + /// @notice Liquidation fee charged by the protocol: fee = totalValue * feeLiquidation uint16 internal feeLiquidation; - /// @dev Multiplier used to compute the total value of funds during liquidation. + /// @notice Multiplier used to compute the total value of funds during liquidation. /// At liquidation, the borrower's funds are discounted, and the pool is paid out of discounted value /// The liquidator takes the difference between the discounted and actual values as premium. uint16 internal liquidationDiscount; - /// @dev Liquidation fee charged by the protocol during liquidation by expiry. Typically lower than feeLiquidation. + /// @notice Liquidation fee charged by the protocol during liquidation by expiry. Typically lower than feeLiquidation. uint16 internal feeLiquidationExpired; - /// @dev Multiplier used to compute the total value of funds during liquidation by expiry. Typically higher than + /// @notice Multiplier used to compute the total value of funds during liquidation by expiry. Typically higher than /// liquidationDiscount (meaning lower premium). uint16 internal liquidationDiscountExpired; - /// @dev Price oracle used to evaluate assets on Credit Accounts. - IPriceOracleV2 public override priceOracle; + /// @notice Price oracle used to evaluate assets on Credit Accounts. + address public override priceOracle; - /// @dev Liquidation threshold for the underlying token. - uint16 internal ltUnderlying; + /// @notice Points to the currently processed Credit Account during multicall, otherwise keeps address(1) for gas savings + /// CreditFacade is a trusted source, so it generally sends the CA as an input for account management functions + /// _activeCreditAccount is used to avoid adapters having to manually pass the Credit Account + address internal _activeCreditAccount; - /// @dev Mask of tokens to apply quotas for - uint256 public override quotedTokenMask; + /// @notice Total number of known collateral tokens. + uint8 public collateralTokensCount; + + /// @notice Mask of tokens to apply quota logic for + uint256 public override quotedTokensMask; - /// @dev Withdrawal manager - IWithdrawalManager public immutable override withdrawalManager; + /// @notice Contract that handles withdrawals + address public immutable override withdrawalManager; - /// @dev Address of the connected Credit Configurator + /// @notice Address of the connected Credit Configurator address public creditConfigurator; /// COLLATERAL TOKENS DATA - /// @dev Map of token's bit mask to its address and LT parameters in a single-slot struct + /// @notice Map of token's bit mask to its address and LT parameters in a single-slot struct mapping(uint256 => CollateralTokenData) internal collateralTokensData; - /// @dev Internal map of token addresses to their indidivual masks. - /// @notice A mask is a uint256 that has only 1 non-zero bit in the position correspondingto + /// @notice Internal map of token addresses to their indidivual masks. + /// @dev A mask is a uint256 that has only 1 non-zero bit in the position corresponding to /// the token's index (i.e., tokenMask = 2 ** index) - /// Masks are used to efficiently check set inclusion, since it only involves + /// Masks are used to efficiently track set inclusion, since it only involves /// a single AND and comparison to zero mapping(address => uint256) internal tokenMasksMapInternal; - /// @dev Total number of known collateral tokens. - uint8 public collateralTokensCount; - /// CONTRACTS & ADAPTERS - /// @dev Maps allowed adapters to their respective target contracts. + /// @notice Maps allowed adapters to their respective target contracts. mapping(address => address) public override adapterToContract; - /// @dev Maps 3rd party contracts to their respective adapters + /// @notice Maps 3rd party contracts to their respective adapters mapping(address => address) public override contractToAdapter; /// CREDIT ACCOUNT DATA - /// @dev Contains infomation related to CA + /// @notice Contains infomation related to CA, such as accumulated interest, + /// enabled tokens, the current borrower and miscellaneous flags mapping(address => CreditAccountInfo) public creditAccountInfo; - /// @dev Array of the allowed contracts - EnumerableSet.AddressSet private creditAccountsSet; + /// @notice Set of all currently active contracts + EnumerableSet.AddressSet internal creditAccountsSet; // // MODIFIERS // - /// @dev Restricts calls to Credit Facade only + /// @notice Restricts calls to Credit Facade only modifier creditFacadeOnly() { - if (msg.sender != creditFacade) revert CallerNotCreditFacadeException(); + _checkCreditFacade(); _; } - /// @dev Restricts calls to Withdrawal Manager only - modifier withdrawalManagerOnly() { - if (msg.sender != address(withdrawalManager)) revert CallerNotWithdrawalManagerException(); - _; + /// @notice Internal function wrapping `creditFacadeOnly` modifier logic + /// Used to optimize contract size + function _checkCreditFacade() private view { + if (msg.sender != creditFacade) revert CallerNotCreditFacadeException(); } - /// @dev Restricts calls to Credit Configurator only + /// @notice Restricts calls to Credit Configurator only modifier creditConfiguratorOnly() { + _checkCreditConfigurator(); + _; + } + + /// @notice Internal function wrapping `creditFacadeOnly` modifier logic + /// Used to optimize contract size + function _checkCreditConfigurator() private view { if (msg.sender != creditConfigurator) { revert CallerNotConfiguratorException(); } - _; } - /// @dev Constructor + /// @notice Constructor + /// @param _addressProvider Address of the repository to get system-level contracts from /// @param _pool Address of the pool to borrow funds from - constructor(address _pool, address _withdrawalManager) { - IAddressProvider addressProvider = IPoolService(_pool).addressProvider(); - - pool = _pool; // F:[CM-1] + constructor(address _addressProvider, address _pool) { + addressProvider = _addressProvider; + pool = _pool; // U:[CM-1] - address _underlying = IPoolService(pool).underlyingToken(); // F:[CM-1] - underlying = _underlying; // F:[CM-1] + underlying = IPoolBase(pool).underlyingToken(); // U:[CM-1] - try IPool4626(pool).supportsQuotas() returns (bool sq) { + try IPoolV3(_pool).supportsQuotas() returns (bool sq) { supportsQuotas = sq; } catch {} // The underlying is the first token added as collateral - _addToken(_underlying); // F:[CM-1] - - wethAddress = addressProvider.getWethToken(); // F:[CM-1] - wethGateway = addressProvider.getWETHGateway(); // F:[CM-1] - - // Price oracle is stored in Slot1, as it is accessed frequently with fees - priceOracle = IPriceOracleV2(addressProvider.getPriceOracle()); // F:[CM-1] - accountFactory = IAccountFactory(addressProvider.getAccountFactory()); // F:[CM-1] - - deployAccountAction = accountFactory.version() == 3_00 ? uint256(TakeAccountAction.DEPLOY_NEW_ONE) : 0; - creditConfigurator = msg.sender; // F:[CM-1] - - withdrawalManager = IWithdrawalManager(_withdrawalManager); - - _externalCallCreditAccount = address(1); + _addToken(underlying); // U:[CM-1] + + weth = + IAddressProviderV3(addressProvider).getAddressOrRevert({key: AP_WETH_TOKEN, _version: NO_VERSION_CONTROL}); // U:[CM-1] + wethGateway = IAddressProviderV3(addressProvider).getAddressOrRevert({key: AP_WETH_GATEWAY, _version: 3_00}); // U:[CM-1] + priceOracle = IAddressProviderV3(addressProvider).getAddressOrRevert({key: AP_PRICE_ORACLE, _version: 2}); // U:[CM-1] + accountFactory = IAddressProviderV3(addressProvider).getAddressOrRevert({ + key: AP_ACCOUNT_FACTORY, + _version: NO_VERSION_CONTROL + }); // U:[CM-1] + withdrawalManager = + IAddressProviderV3(addressProvider).getAddressOrRevert({key: AP_WITHDRAWAL_MANAGER, _version: 3_00}); + + creditConfigurator = msg.sender; // U:[CM-1] + + _activeCreditAccount = address(1); } // // CREDIT ACCOUNT MANAGEMENT // - /// @dev Opens credit account and borrows funds from the pool. + /// @notice Opens credit account and borrows funds from the pool. /// - Takes Credit Account from the factory; + /// - Initializes `creditAccountInfo` fields /// - Requests the pool to lend underlying to the Credit Account /// /// @param debt Amount to be borrowed by the Credit Account /// @param onBehalfOf The owner of the newly opened Credit Account - function openCreditAccount(uint256 debt, address onBehalfOf, bool deployNew) + function openCreditAccount(uint256 debt, address onBehalfOf) external override - nonReentrant - creditFacadeOnly // F:[CM-2] + nonReentrant // U:[CM-5] + creditFacadeOnly // // U:[CM-2] returns (address creditAccount) { - // Takes a Credit Account from the factory and sets initial parameters - // The Credit Account will be connected to this Credit Manager until closing - creditAccount = accountFactory.takeCreditAccount(deployNew ? deployAccountAction : 0, 0); // F:[CM-8] - - creditAccountInfo[creditAccount].debt = debt; - creditAccountInfo[creditAccount].cumulativeIndexLastUpdate = IPoolService(pool).calcLinearCumulative_RAY(); - creditAccountInfo[creditAccount].borrower = onBehalfOf; + creditAccount = IAccountFactory(accountFactory).takeCreditAccount(0, 0); // U:[CM-6] - if (supportsQuotas) creditAccountInfo[creditAccount].cumulativeQuotaInterest = 1; // F: [CMQ-1] + creditAccountInfo[creditAccount].debt = debt; // U:[CM-6] + creditAccountInfo[creditAccount].cumulativeIndexLastUpdate = _poolCumulativeIndexNow(); // U:[CM-6] + creditAccountInfo[creditAccount].flags = 0; // U:[CM-6] + creditAccountInfo[creditAccount].borrower = onBehalfOf; // U:[CM-6] - // Initializes the enabled token mask for Credit Account to 1 (only the underlying is enabled) - // OUTDATED: enabledTokensMap is set in FullCollateralCheck - // enabledTokensMap[creditAccount] = 1; // F:[CM-8] + if (supportsQuotas) { + creditAccountInfo[creditAccount].cumulativeQuotaInterest = 1; + } // U:[CM-6] // Requests the pool to transfer tokens the Credit Account - IPoolService(pool).lendCreditAccount(debt, creditAccount); // F:[CM-8] - creditAccountsSet.add(creditAccount); + _poolLendCreditAccount(debt, creditAccount); // U:[CM-6] + creditAccountsSet.add(creditAccount); // U:[CM-6] } - /// @dev Closes a Credit Account - covers both normal closure and liquidation - /// - Checks whether the contract is paused, and, if so, if the payer is an emergency liquidator. - /// Only emergency liquidators are able to liquidate account while the CM is paused. - /// Emergency liquidations do not pay a liquidator premium or liquidation fees. - /// - Calculates payments to various recipients on closure: - /// + Computes amountToPool, which is the amount to be sent back to the pool. + /// @notice Closes a Credit Account - covers both normal closure and liquidation + /// - Calculates payments to various recipients on closure. + /// + amountToPool is the amount to be sent back to the pool. /// This includes the principal, interest and fees, but can't be more than /// total position value /// + Computes remainingFunds during liquidations - these are leftover funds /// after paying the pool and the liquidator, and are sent to the borrower /// + Computes protocol profit, which includes interest and liquidation fees /// + Computes loss if the totalValue is less than borrow amount + interest + /// remainingFunds and loss are only computed during liquidation, since they are + /// meaningless during normal account closure. /// - Checks the underlying token balance: /// + if it is larger than amountToPool, then the pool is paid fully from funds on the Credit Account /// + else tries to transfer the shortfall from the payer - either the borrower during closure, or liquidator during liquidation + /// - Signals the pool that the debt is repaid + /// - If liquidation: transfers `remainingFunds` to the borrower + /// - If the account has active quotas, requests the PoolQuotaKeeper to reduce them to 0, + /// which is required for correctness of interest calculations /// - Send assets to the "to" address, as long as they are not included into skipTokenMask - /// - If convertWETH is true, the function converts WETH into ETH before sending /// - Returns the Credit Account back to factory /// /// @param creditAccount Credit account address @@ -285,7 +294,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard /// @param payer Address which would be charged if credit account has not enough funds to cover amountToPool /// @param to Address to which the leftover funds will be sent /// @param skipTokensMask Tokenmask contains 1 for tokens which needed to be send directly - /// @param convertWETH If true converts WETH to ETH + /// @param convertToETH If true converts WETH to ETH function closeCreditAccount( address creditAccount, ClosureAction closureAction, @@ -293,28 +302,27 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard address payer, address to, uint256 skipTokensMask, - bool convertWETH + bool convertToETH ) external override - nonReentrant - creditFacadeOnly // F:[CM-2] + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] returns (uint256 remainingFunds, uint256 loss) { // Checks that the Credit Account exists for the borrower - address borrower = getBorrowerOrRevert(creditAccount); // F:[CM-6, 9, 10] + address borrower = getBorrowerOrRevert(creditAccount); // U:[CM-7] // Sets borrower's Credit Account to zero address - creditAccountInfo[creditAccount].borrower = address(0); // F:[CM-9] - creditAccountInfo[creditAccount].flags = 0; - - // Makes all computations needed to close credit account + delete creditAccountInfo[creditAccount].borrower; // U:[CM-8] uint256 amountToPool; uint256 profit; + // Computations for various closure amounts are isolated into the `CreditLogic` library + // See `CreditLogic.calcClosePayments` and `CreditLogic.calcLiquidationPayments` if (closureAction == ClosureAction.CLOSE_ACCOUNT) { - (amountToPool, profit) = collateralDebtData.calcClosePayments({amountWithFeeFn: _amountWithFee}); + (amountToPool, profit) = collateralDebtData.calcClosePayments({amountWithFeeFn: _amountWithFee}); // U:[CM-8] } else { // During liquidation, totalValue of the account is discounted // by (1 - liquidationPremium). This means that totalValue * liquidationPremium @@ -332,222 +340,300 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard : liquidationDiscountExpired, feeLiquidation: closureAction == ClosureAction.LIQUIDATE_ACCOUNT ? feeLiquidation : feeLiquidationExpired, amountWithFeeFn: _amountWithFee, - amountMinusFeeFn: _amountWithFee - }); + amountMinusFeeFn: _amountMinusFee + }); // U:[CM-8] } + { + uint256 underlyingBalance = IERC20Helper.balanceOf({token: underlying, holder: creditAccount}); // U:[CM-8] - uint256 underlyingBalance = IERC20(underlying)._balanceOf(creditAccount); - - if (underlyingBalance > amountToPool + remainingFunds + 1) { - // If there is an underlying surplus, transfers it to the "to" address - unchecked { - _safeTokenTransfer( - creditAccount, underlying, to, underlyingBalance - amountToPool - remainingFunds - 1, convertWETH - ); // F:[CM-10,12,16] - } - } else if (underlyingBalance < amountToPool + remainingFunds + 1) { // If there is an underlying shortfall, attempts to transfer it from the payer - unchecked { - IERC20(underlying).safeTransferFrom( - payer, creditAccount, _amountWithFee(amountToPool + remainingFunds - underlyingBalance + 1) - ); // F:[CM-11,13] + if (underlyingBalance < amountToPool + remainingFunds + 1) { + unchecked { + IERC20(underlying).safeTransferFrom({ + from: payer, + to: creditAccount, + value: _amountWithFee(amountToPool + remainingFunds + 1 - underlyingBalance) + }); // U:[CM-8] + } } } // Transfers the due funds to the pool - _safeTokenTransfer(creditAccount, underlying, pool, amountToPool, false); // F:[CM-10,11,12,13] + ICreditAccount(creditAccount).transfer({token: underlying, to: pool, amount: amountToPool}); // U:[CM-8] // Signals to the pool that debt has been repaid. The pool relies // on the Credit Manager to repay the debt correctly, and does not // check internally whether the underlying was actually transferred - IPoolService(pool).repayCreditAccount(collateralDebtData.debt, profit, loss); // F:[CM-10,11,12,13] + _poolRepayCreditAccount(collateralDebtData.debt, profit, loss); // U:[CM-8] // transfer remaining funds to the borrower [liquidations only] if (remainingFunds > 1) { - _safeTokenTransfer(creditAccount, underlying, borrower, remainingFunds, false); // F:[CM-13,18] + _safeTokenTransfer({ + creditAccount: creditAccount, + token: underlying, + to: borrower, + amount: remainingFunds, + convertToETH: false + }); // U:[CM-8] } - if (supportsQuotas && collateralDebtData.quotedTokens.length > 0) { - /// In case of amy loss, PQK sets limits to zero for all quoted tokens - bool setLimitsToZero = loss > 0; - poolQuotaKeeper().removeQuotas({ + // If the creditAccount has non-zero quotas, they need to be reduced to 0; + // This is required to both free quota limits for other users and correctly + // compute quota interest + if (supportsQuotas && collateralDebtData.quotedTokens.length != 0) { + /// In case of any loss, PQK sets limits to zero for all quoted tokens + bool setLimitsToZero = loss > 0; // U:[CM-8] + + IPoolQuotaKeeper(collateralDebtData._poolQuotaKeeper).removeQuotas({ creditAccount: creditAccount, tokens: collateralDebtData.quotedTokens, setLimitsToZero: setLimitsToZero - }); // F: [CMQ-6] + }); // U:[CM-8] } - uint256 enabledTokensMask = collateralDebtData.enabledTokensMask & ~skipTokensMask; - - _transferAssetsTo(creditAccount, to, convertWETH, enabledTokensMask); // F:[CM-14,17,19] + // All remaining assets on the account are transferred to the `to` address + // If some asset cannot be transferred directly (e.g., `to` is blacklisted by USDC), + // then an immediate withdrawal is added to withdrawal manager + _batchTokensTransfer({ + creditAccount: creditAccount, + to: to, + convertToETH: convertToETH, + tokensToTransferMask: collateralDebtData.enabledTokensMask.disable(skipTokensMask) + }); // U:[CM-8, 9] // Returns Credit Account to the factory - accountFactory.returnCreditAccount(creditAccount); // F:[CM-9] - creditAccountsSet.remove(creditAccount); + IAccountFactory(accountFactory).returnCreditAccount({usedAccount: creditAccount}); // U:[CM-8] + creditAccountsSet.remove(creditAccount); // U:[CM-8] } - /// @dev Manages debt size for borrower: + /// @notice Manages debt size for borrower: /// /// - Increase debt: /// + Increases debt by transferring funds from the pool to the credit account /// + Updates the cumulative index to keep interest the same. Since interest /// is always computed dynamically as debt * (cumulativeIndexNew / cumulativeIndexOpen - 1), - /// cumulativeIndexOpen needs to be updated, as the borrow amount has changed + /// cumulativeIndexOpen needs to be updated, as the debt amount has changed /// /// - Decrease debt: - /// + Repays debt partially + all interest and fees accrued thus far - /// + Updates cunulativeIndex to cumulativeIndex now + /// + Repays the debt in the following order: quota interest + fees, normal interest + fees, debt; + /// In case of interest, if the (remaining) amount is not enough to cover it fully, + /// it is split pro-rata between interest and fees to preserve correct fee computations + /// + If there were non-zer quota interest, updates to quota interest after repayment + /// + If base interest was repaid, updates `cumulativeIndexLastUpdate` + /// + If debt was repaid, updates `debt` + /// @dev For details on cumulativeIndex computations, see `CreditLogic.calcIncrease` and `CreditLogic.calcDecrease` /// /// @param creditAccount Address of the Credit Account to change debt for - /// @param amount Amount to increase / decrease the principal by - /// @param action Increase/decrease bed debt + /// @param amount Amount to increase / decrease the total debt by + /// @param enabledTokensMask The current enabledTokensMask (required for quota interest computation) + /// @param action Whether to increase or decrease debt /// @return newDebt The new debt principal + /// @return tokensToEnable The mask of tokens enabled after the operation + /// @return tokensToDisable The mask of tokens disabled after the operation function manageDebt(address creditAccount, uint256 amount, uint256 enabledTokensMask, ManageDebtAction action) external - nonReentrant - creditFacadeOnly // F:[CM-2] + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] returns (uint256 newDebt, uint256 tokensToEnable, uint256 tokensToDisable) { - (uint256 debt, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow) = - _getCreditAccountParameters(creditAccount); + CollateralDebtData memory collateralDebtData; + uint256[] memory collateralHints; uint256 newCumulativeIndex; if (action == ManageDebtAction.INCREASE_DEBT) { - (newDebt, newCumulativeIndex) = - CreditLogic.calcIncrease(debt, amount, cumulativeIndexNow, cumulativeIndexLastUpdate); + /// INCREASE DEBT - // Requests the pool to lend additional funds to the Credit Account - IPoolService(pool).lendCreditAccount(amount, creditAccount); // F:[CM-20] - tokensToEnable = UNDERLYING_TOKEN_MASK; + collateralDebtData = _calcDebtAndCollateral({ + creditAccount: creditAccount, + enabledTokensMask: enabledTokensMask, + collateralHints: collateralHints, + minHealthFactor: PERCENTAGE_FACTOR, + task: CollateralCalcTask.GENERIC_PARAMS + }); // U:[CM-10] + + (newDebt, newCumulativeIndex) = CreditLogic.calcIncrease({ + amount: amount, + debt: collateralDebtData.debt, + cumulativeIndexNow: collateralDebtData.cumulativeIndexNow, + cumulativeIndexLastUpdate: collateralDebtData.cumulativeIndexLastUpdate + }); // U:[CM-10] + + _poolLendCreditAccount(amount, creditAccount); // U:[CM-10] + + tokensToEnable = UNDERLYING_TOKEN_MASK; // U:[CM-10] } else { - // Decrease - uint256 cumulativeQuotaInterest; + // DECREASE DEBT - if (supportsQuotas) { - cumulativeQuotaInterest = creditAccountInfo[creditAccount].cumulativeQuotaInterest - 1; - { - (address[] memory tokens,) = - _getQuotedTokens({enabledTokensMask: enabledTokensMask, withLTs: false}); - if (tokens.length > 0) { - cumulativeQuotaInterest += poolQuotaKeeper().accrueQuotaInterest(creditAccount, tokens); // F: [CMQ-4,5] - } - } - } - - // Pays the amount back to the pool - ICreditAccount(creditAccount)._safeTransfer(underlying, pool, amount); // F:[CM-21] + collateralDebtData = _calcDebtAndCollateral({ + creditAccount: creditAccount, + enabledTokensMask: enabledTokensMask, + collateralHints: collateralHints, + minHealthFactor: PERCENTAGE_FACTOR, + task: CollateralCalcTask.DEBT_ONLY + }); // U:[CM-11] + + uint256 newCumulativeQuotaInterest; + // Pays the entire amount back to the pool + ICreditAccount(creditAccount).transfer({token: underlying, to: pool, amount: amount}); // U:[CM-11] { uint256 amountToRepay; uint256 profit; - (newDebt, newCumulativeIndex, amountToRepay, profit, cumulativeQuotaInterest) = CreditLogic - .calcDescrease({ - amount: amount, - quotaInterestAccrued: cumulativeQuotaInterest, - feeInterest: feeInterest, - debt: debt, - cumulativeIndexNow: cumulativeIndexNow, - cumulativeIndexLastUpdate: cumulativeIndexLastUpdate - }); - - IPoolService(pool).repayCreditAccount(amountToRepay, profit, 0); // F:[CM-21] + (newDebt, newCumulativeIndex, amountToRepay, profit, newCumulativeQuotaInterest) = CreditLogic + .calcDecrease({ + amount: _amountMinusFee(amount), + debt: collateralDebtData.debt, + cumulativeIndexNow: collateralDebtData.cumulativeIndexNow, + cumulativeIndexLastUpdate: collateralDebtData.cumulativeIndexLastUpdate, + cumulativeQuotaInterest: collateralDebtData.cumulativeQuotaInterest, + feeInterest: feeInterest + }); // U:[CM-11] + + /// @dev amountToRepay is whatever is left after repaying quota and base interest + fees + _poolRepayCreditAccount(amountToRepay, profit, 0); // U:[CM-11] } + + /// If quota logic is supported, we need to accrue quota interest in order to keep + /// quota interest indexes in PQK and cumulativeQuotaInterest in Credit Manager consistent + /// with each other, since this action caches all quota interest in Credit Manager if (supportsQuotas) { - creditAccountInfo[creditAccount].cumulativeQuotaInterest = cumulativeQuotaInterest; + IPoolQuotaKeeper(collateralDebtData._poolQuotaKeeper).accrueQuotaInterest({ + creditAccount: creditAccount, + tokens: collateralDebtData.quotedTokens + }); + creditAccountInfo[creditAccount].cumulativeQuotaInterest = newCumulativeQuotaInterest + 1; // U:[CM-11] } - if (IERC20(underlying)._balanceOf(creditAccount) <= 1) { - tokensToDisable = UNDERLYING_TOKEN_MASK; + /// If the entire underlying balance was spent on repayment, it is disabled + if (IERC20Helper.balanceOf({token: underlying, holder: creditAccount}) <= 1) { + tokensToDisable = UNDERLYING_TOKEN_MASK; // U:[CM-11] } } - // - // Sets new parameters on the Credit Account if they were changed - if (newDebt != debt || newCumulativeIndex != cumulativeIndexLastUpdate) { - creditAccountInfo[creditAccount].debt = newDebt; // F:[CM-20. 21] - creditAccountInfo[creditAccount].cumulativeIndexLastUpdate = newCumulativeIndex; // F:[CM-20. 21] - } + + creditAccountInfo[creditAccount].debt = newDebt; // U:[CM-10, 11] + creditAccountInfo[creditAccount].cumulativeIndexLastUpdate = newCumulativeIndex; // U:[CM-10, 11] } - /// @dev Adds collateral to borrower's credit account + /// @notice Transfer collateral from the payer to the credit account /// @param payer Address of the account which will be charged to provide additional collateral /// @param creditAccount Address of the Credit Account /// @param token Collateral token to add /// @param amount Amount to add function addCollateral(address payer, address creditAccount, address token, uint256 amount) external - nonReentrant - creditFacadeOnly // F:[CM-2] + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] returns (uint256 tokenMask) { - tokenMask = getTokenMaskOrRevert(token); - IERC20(token).safeTransferFrom(payer, creditAccount, amount); // F:[CM-22] + tokenMask = getTokenMaskOrRevert({token: token}); // U:[CM-13] + IERC20(token).safeTransferFrom({from: payer, to: creditAccount, value: amount}); // U:[CM-13] } - /// @dev Transfers Credit Account ownership to another address + /// @notice Transfers Credit Account ownership to another address /// @param creditAccount Address of creditAccount to be transferred /// @param to Address of new owner function transferAccountOwnership(address creditAccount, address to) external override - nonReentrant - creditFacadeOnly // F:[CM-2] + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] { - if (creditAccountInfo[creditAccount].borrower == address(0)) { - revert CreditAccountNotExistsException(); - } // F:[CM-7] - creditAccountInfo[creditAccount].borrower = to; // F:[CM-7] + getBorrowerOrRevert({creditAccount: creditAccount}); // U:[CM-14] + creditAccountInfo[creditAccount].borrower = to; // U:[CM-14] } - /// @dev Requests the Credit Account to approve a collateral token to another contract. + /// + /// ADAPTER FUNCTIONS + /// + + /// @notice Requests the Credit Account to approve a collateral token to another contract. /// @param token Collateral token to approve /// @param amount New allowance amount - function approveCreditAccount(address token, uint256 amount) external override nonReentrant { - address targetContract = _getTargetContractOrRevert(); - - _approveSpender(token, targetContract, externalCallCreditAccountOrRevert(), amount); + function approveCreditAccount(address token, uint256 amount) + external + override + nonReentrant // U:[CM-5] + { + address targetContract = _getTargetContractOrRevert(); // U:[CM-3] + _approveSpender({ + creditAccount: getActiveCreditAccountOrRevert(), + token: token, + spender: targetContract, + amount: amount + }); // U:[CM-15] } - function _approveSpender(address token, address targetContract, address creditAccount, uint256 amount) internal { + /// @notice Internal wrapper for approving tokens, used to optimize contract size, since approvals + /// are used in several functions + /// @param creditAccount Address of the Credit Account + /// @param token Token to give an approval for + /// @param spender The address of the spender + /// @param amount The new allowance amount + function _approveSpender(address creditAccount, address token, address spender, uint256 amount) internal { // Checks that the token is a collateral token // Forbidden tokens can be approved, since users need that to // sell them off - getTokenMaskOrRevert(token); + getTokenMaskOrRevert({token: token}); // U:[CM-15] - ICreditAccount(creditAccount).safeApprove(token, targetContract, amount); + // The approval logic is isolated into `CreditAccountHelper.safeApprove`. See the corresponding + // library for details + ICreditAccount(creditAccount).safeApprove({token: token, spender: spender, amount: amount}); // U:[CM-15] } - /// @dev Requests a Credit Account to make a low-level call with provided data + /// @notice Requests a Credit Account to make a low-level call with provided data /// This is the intended pathway for state-changing interactions with 3rd-party protocols /// @param data Data to pass with the call - function executeOrder(bytes memory data) external override nonReentrant returns (bytes memory) { - address targetContract = _getTargetContractOrRevert(); + function executeOrder(bytes calldata data) + external + override + nonReentrant // U:[CM-5] + returns (bytes memory) + { + address targetContract = _getTargetContractOrRevert(); // U:[CM-3] // Emits an event - emit ExecuteOrder(targetContract); // F:[CM-29] + emit ExecuteOrder(targetContract); // U:[CM-16] // Returned data is provided as-is to the caller; // It is expected that is is parsed and returned as a correct type // by the adapter itself. - return ICreditAccount(externalCallCreditAccountOrRevert()).execute(targetContract, data); // F:[CM-29] + address creditAccount = getActiveCreditAccountOrRevert(); // U:[CM-16] + return ICreditAccount(creditAccount).execute(targetContract, data); // U:[CM-16] } + /// @notice Returns the target contract associated with the calling address (which is assumed to be an adapter), + /// and reverts if there is none. Used to ensure that an adapter can only make calls to its own target function _getTargetContractOrRevert() internal view returns (address targetContract) { - targetContract = adapterToContract[msg.sender]; - - // Checks that msg.sender is the adapter associated with the passed - // target contract. + targetContract = adapterToContract[msg.sender]; // U:[CM-15, 16] if (targetContract == address(0)) { - revert CallerNotAdapterException(); - // F:[CM-28] + revert CallerNotAdapterException(); // U:[CM-3] } } + /// @notice Sets the active credit account (credit account returned to adapters to work on) to the provided address + /// @dev CreditFacade must always ensure that `_activeCreditAccount` is address(1) between calls + function setActiveCreditAccount(address creditAccount) + external + override + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] + { + _activeCreditAccount = creditAccount; + } + + /// @notice Returns the current active credit account + function getActiveCreditAccountOrRevert() public view override returns (address creditAccount) { + creditAccount = _activeCreditAccount; + if (creditAccount == address(1)) revert ActiveCreditAccountNotSetException(); + } + // // COLLATERAL VALIDITY AND ACCOUNT HEALTH CHECKS // - /// @dev Performs a full health check on an account with a custom order of evaluated tokens and + /// @notice Performs a full health check on an account with a custom order of evaluated tokens and /// a custom minimal health factor /// @param creditAccount Address of the Credit Account to check + /// @param enabledTokensMask Current enabled token mask /// @param collateralHints Array of token masks in the desired order of evaluation /// @param minHealthFactor Minimal health factor of the account, in PERCENTAGE format function fullCollateralCheck( @@ -555,323 +641,482 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard uint256 enabledTokensMask, uint256[] memory collateralHints, uint16 minHealthFactor - ) external creditFacadeOnly nonReentrant { + ) + external + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] + { if (minHealthFactor < PERCENTAGE_FACTOR) { - revert CustomHealthFactorTooLowException(); + revert CustomHealthFactorTooLowException(); // U:[CM-17] } - CollateralDebtData memory collateralDebtData = - _calcFullCollateral(creditAccount, enabledTokensMask, minHealthFactor, collateralHints, priceOracle, true); - - if (collateralDebtData.isLiquidatable) { - revert NotEnoughCollateralException(); + /// Performs a generalized debt and collteral computation with the + /// task FULL_COLLATERAL_CHECK_LAZY. This ensures that collateral computations + /// stop as soon as it is determined that there is enough collateral to cover the debt, + /// which is done in order to save gas + CollateralDebtData memory collateralDebtData = _calcDebtAndCollateral({ + creditAccount: creditAccount, + minHealthFactor: minHealthFactor, + collateralHints: collateralHints, + enabledTokensMask: enabledTokensMask, + task: CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY + }); // U:[CM-18] + + /// If the TWV value outputted by the collateral computation is less than + /// total debt, the full collateral check has failed + if (collateralDebtData.twvUSD < collateralDebtData.totalDebtUSD) { + revert NotEnoughCollateralException(); // U:[CM-18] } - _saveEnabledTokensMask(creditAccount, collateralDebtData.enabledTokensMask); + /// During the debt and collateral computation, any encountered zero balance + /// tokens are deleted from the in-memory enabledTokensMask, so the final value + /// must be saved + _saveEnabledTokensMask(creditAccount, collateralDebtData.enabledTokensMask); // U:[CM-18] } - /// @dev Calculates total value for provided Credit Account in underlying - /// More: https://dev.gearbox.fi/developers/credit/economy#totalUSD-value - /// - /// @param creditAccount Credit Account address - // @return total Total value in underlying - // @return twv Total weighted (discounted by liquidation thresholds) value in underlying + /// TODO: Q: move to CF? + /// @notice Returns whether the passed credit account is unhealthy given the provided minHealthFactor + /// @param creditAccount Address of the credit account to check + /// @param minHealthFactor The health factor below which the function would + /// consider the account unhealthy + function isLiquidatable(address creditAccount, uint16 minHealthFactor) external view override returns (bool) { + uint256[] memory collateralHints; + + CollateralDebtData memory collateralDebtData = _calcDebtAndCollateral({ + creditAccount: creditAccount, + enabledTokensMask: enabledTokensMaskOf(creditAccount), + collateralHints: collateralHints, + minHealthFactor: minHealthFactor, + task: CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY + }); // U:[CM-18] + + return collateralDebtData.twvUSD < collateralDebtData.totalDebtUSD; // U:[CM-18] + } + + /// @notice Calculates parameters related to account's debt and collateral, + /// with level of detail dependent on `task` + /// @dev Unlike previous versions, Gearbox V3 uses a generalized Credit Manager function to compute debt and collateral + /// that is then reused in other contracts (including third-party contracts, such as bots). This allows to ensure that account health + /// computation logic is uniform across the codebase. The returned collateralDebtData object is intended to be a complete set of + /// data required to track account health, but can be filled partially to avoid unnecessary computation where needed. + /// @param creditAccount Credit Account to compute parameters for + /// @param task Determines the parameters to compute: + /// * GENERIC_PARAMS - computes debt and raw base interest indexes + /// * DEBT_ONLY - computes all debt parameters, including totalDebt, accrued base/quota interest and associated fees; + /// if quota logic is enabled, also returns all data relevant to quota interest and collateral computations in the struct + /// * DEBT_COLLATERAL_WITHOUT_WITHDRAWALS - computes all debt parameters and the total value of collateral + /// (both weighted and unweighted, in USD and underlying). + /// * DEBT_COLLATERAL_CANCEL_WITHDRAWALS - same as above, but includes immature withdrawals into the total value of the Credit Account; + /// NB: The value of withdrawals is not included into the total weighted value, so they have no bearing on whether an account can be + /// liquidated. However, during liquidations it is prudent to return immature withdrawals to the Credit Account, to defend against attacks + /// that involve withdrawals combined with oracle manipulations. Hence, this collateral computation method is used for liquidations. + /// * DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS - same as above, but also includes mature withdrawals into the total value of the Credit Account; + /// NB: This method of calculation is used for emergency liquidations. Emergency liquidations are performed when the system is paused due to a + /// perceived security risk. Returning mature withdrawals is a contingency for the case where a malicious withdrawal is scheduled, the system + /// is paused, and the withdrawal matures while the DAO coordinates a response. + /// @return collateralDebtData A struct containing debt and collateral parameters. It is filled based on the passed task. + /// For more information on struct fields, see its definition along the `ICreditManagerV3` interface function calcDebtAndCollateral(address creditAccount, CollateralCalcTask task) external view override returns (CollateralDebtData memory collateralDebtData) { - uint256 enabledTokensMask = enabledTokensMaskOf(creditAccount); - - if (task == CollateralCalcTask.DEBT_ONLY) { - uint256 quotaInterest; - - if (supportsQuotas) { - (address[] memory tokens,) = _getQuotedTokens({enabledTokensMask: enabledTokensMask, withLTs: false}); - - quotaInterest = creditAccountInfo[creditAccount].cumulativeQuotaInterest - 1; - - if (tokens.length > 0) { - quotaInterest += poolQuotaKeeper().outstandingQuotaInterest(creditAccount, tokens); // F: [CMQ-10] - } - - collateralDebtData.quotedTokens = tokens; - } - - (collateralDebtData.debt, collateralDebtData.accruedInterest, collateralDebtData.accruedFees) = - _calcCreditAccountAccruedInterest(creditAccount, quotaInterest); - } else { - IPriceOracleV2 _priceOracle = priceOracle; - uint256[] memory collateralHints; - - collateralDebtData = _calcFullCollateral( - creditAccount, enabledTokensMask, PERCENTAGE_FACTOR, collateralHints, _priceOracle, false - ); - - if ( - ( - task == CollateralCalcTask.DEBT_COLLATERAL_CANCEL_WITHDRAWALS - || task == CollateralCalcTask.DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS - ) && _hasWithdrawals(creditAccount) - ) { - collateralDebtData.totalValueUSD += _calcCancellableWithdrawalsValue( - _priceOracle, creditAccount, task == CollateralCalcTask.DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS - ); - } - - collateralDebtData.totalValue = _convertFromUSD(_priceOracle, collateralDebtData.totalValueUSD, underlying); // F:[FA-41] + uint256[] memory collateralHints; + + /// @dev FULL_COLLATERAL_CHECK_LAZY is a special calculation type + /// that can only be used safely internally, since it can stop early + /// and possibly return incorrect TWV/TV values. Therefore, it is + /// prevented from being called internally + if (task == CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY) { + revert IncorrectParameterException(); // U:[CM-19] } - // FINALLY - collateralDebtData.enabledTokensMask = enabledTokensMask; + collateralDebtData = _calcDebtAndCollateral({ + creditAccount: creditAccount, + enabledTokensMask: enabledTokensMaskOf(creditAccount), + collateralHints: collateralHints, + minHealthFactor: PERCENTAGE_FACTOR, + task: task + }); // U:[CM-20] } - /// @dev Calculates total value for provided Credit Account in USD - // @param _priceOracle Oracle used to convert assets to USD - // @param creditAccount Address of the Credit Account - - function _calcFullCollateral( + /// @notice Implementation for `calcDebtAndCollateral`. + /// @param creditAccount Credit Account to compute collateral for + /// @param enabledTokensMask Current enabled tokens mask + /// @param collateralHints Array of token masks in the desired order of evaluation. + /// Used to optimize the length of lazy evaluation by putting the most valuable + /// tokens on the account first. + /// @param minHealthFactor The health factor to stop the lazy evaluation at + /// @param task The type of calculation to perform (see `calcDebtAndCollateral` for details) + function _calcDebtAndCollateral( address creditAccount, uint256 enabledTokensMask, - uint16 minHealthFactor, uint256[] memory collateralHints, - IPriceOracleV2 _priceOracle, - bool lazy + uint16 minHealthFactor, + CollateralCalcTask task ) internal view returns (CollateralDebtData memory collateralDebtData) { - uint256 totalUSD; - uint256 twvUSD; - - uint256 quotaInterest; + /// GENERIC PARAMS + /// The generic parameters include the debt principal and base interest current and LU indexes + /// This is the minimal amount of debt data required to perform computations after increasing debt. + collateralDebtData.debt = creditAccountInfo[creditAccount].debt; // U:[CM-20] + collateralDebtData.cumulativeIndexLastUpdate = creditAccountInfo[creditAccount].cumulativeIndexLastUpdate; // U:[CM-20] + collateralDebtData.cumulativeIndexNow = _poolCumulativeIndexNow(); // U:[CM-20] + + if (task == CollateralCalcTask.GENERIC_PARAMS) { + return collateralDebtData; + } // U:[CM-20] + + /// DEBT + /// Debt parameters include accrued interest (with quota interest included, if applicable) and fees + /// Parameters related to quoted tokens are cached inside the struct, since they are read from storage + /// during quota interest computation and can be later reused to compute quota token collateral + collateralDebtData.enabledTokensMask = enabledTokensMask; // U:[CM-21] if (supportsQuotas) { - (totalUSD, twvUSD, quotaInterest, collateralDebtData.quotedTokens) = - _calcQuotedCollateral(creditAccount, enabledTokensMask, _priceOracle); + collateralDebtData._poolQuotaKeeper = poolQuotaKeeper(); // U:[CM-21] + + ( + collateralDebtData.quotedTokens, + collateralDebtData.cumulativeQuotaInterest, + collateralDebtData.quotas, + collateralDebtData.quotedLts, + collateralDebtData.quotedTokensMask + ) = _getQuotedTokensData({ + creditAccount: creditAccount, + enabledTokensMask: enabledTokensMask, + _poolQuotaKeeper: collateralDebtData._poolQuotaKeeper + }); // U:[CM-21] + + collateralDebtData.cumulativeQuotaInterest += creditAccountInfo[creditAccount].cumulativeQuotaInterest - 1; // U:[CM-21] } - // The total weighted value of a Credit Account has to be compared - // with the entire debt sum, including interest and fees - (collateralDebtData.debt, collateralDebtData.accruedInterest, collateralDebtData.accruedFees) = - _calcCreditAccountAccruedInterest(creditAccount, quotaInterest); - - uint256 debtPlusInterestRateAndFeesUSD = _convertToUSD( - _priceOracle, - collateralDebtData.calcTotalDebt() * minHealthFactor, // F: [CM-42] - underlying - ) / PERCENTAGE_FACTOR; - - // If quoted tokens fully cover the debt, we can stop here - // after performing some additional cleanup - if (twvUSD < debtPlusInterestRateAndFeesUSD || !lazy) { - uint256 _totalUSD; - uint256 _twvUSD; - uint256 limit = lazy ? (debtPlusInterestRateAndFeesUSD - twvUSD) : type(uint256).max; - - uint256 tokensToDisable; - (tokensToDisable, _totalUSD, _twvUSD) = - _calcNotQuotedCollateral(creditAccount, enabledTokensMask, limit, collateralHints, _priceOracle); - - /// @notice tokensToDisable doesn't have any quoted tokens, no need to mask it - enabledTokensMask = enabledTokensMask.disable(tokensToDisable); - totalUSD += _totalUSD; - twvUSD += _twvUSD; + collateralDebtData.accruedInterest = CreditLogic.calcAccruedInterest({ + amount: collateralDebtData.debt, + cumulativeIndexLastUpdate: collateralDebtData.cumulativeIndexLastUpdate, + cumulativeIndexNow: collateralDebtData.cumulativeIndexNow + }) + collateralDebtData.cumulativeQuotaInterest; // U:[CM-21] + + collateralDebtData.accruedFees = (collateralDebtData.accruedInterest * feeInterest) / PERCENTAGE_FACTOR; // U:[CM-21] + + if (task == CollateralCalcTask.DEBT_ONLY) return collateralDebtData; // U:[CM-21] + + /// COLLATERAL + /// Collateral values such as total value / total weighted value are computed and saved into the struct + /// And zero-balance tokens encountered are removed from enabledTokensMask inside the struct as well + /// If the task is FULL_COLLATERAL_CHECK_LAZY, then collateral value are only computed until twvUSD > totalDebtUSD, + /// and any extra collateral on top of that is not included into the account's value + address _priceOracle = priceOracle; + + collateralDebtData.totalDebtUSD = _convertToUSD({ + _priceOracle: _priceOracle, + amountInToken: collateralDebtData.calcTotalDebt(), + token: underlying + }); // U:[CM-22] + + /// The logic for computing collateral is isolated into the `CreditLogic` library. See `CreditLogic.calcCollateral` for details. + uint256 tokensToDisable; + (collateralDebtData.totalValueUSD, collateralDebtData.twvUSD, tokensToDisable) = collateralDebtData + .calcCollateral({ + creditAccount: creditAccount, + underlying: underlying, + lazy: task == CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY, + minHealthFactor: minHealthFactor, + collateralHints: collateralHints, + priceOracle: _priceOracle, + collateralTokensByMaskFn: _collateralTokensByMask, + convertToUSDFn: _convertToUSD + }); // U:[CM-22] + + collateralDebtData.enabledTokensMask = enabledTokensMask.disable(tokensToDisable); // U:[CM-22] + + if (task == CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY) { + return collateralDebtData; } - collateralDebtData.totalValueUSD = totalUSD; - collateralDebtData.twvUSD = twvUSD; + /// WITHDRAWALS + /// Withdrawals are added to the total value of the account primarily for liquidation purposes, + /// since we want to return withdrawals to the Credit Account but also need to ensure that + /// they are included into remainingFunds. + if ((task != CollateralCalcTask.DEBT_COLLATERAL_WITHOUT_WITHDRAWALS) && _hasWithdrawals(creditAccount)) { + collateralDebtData.totalValueUSD += _addCancellableWithdrawalsValue({ + _priceOracle: _priceOracle, + creditAccount: creditAccount, + isForceCancel: task == CollateralCalcTask.DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS + }); // U:[CM-23] + } - collateralDebtData.enabledTokensMask = enabledTokensMask; - collateralDebtData.hf = uint16(collateralDebtData.twvUSD * PERCENTAGE_FACTOR / debtPlusInterestRateAndFeesUSD); - collateralDebtData.isLiquidatable = twvUSD < debtPlusInterestRateAndFeesUSD; + /// The underlying-denominated total value is also added for liquidation payments calculations, unless the data is computed + /// for fullCollateralCheck, which doesn't need this + collateralDebtData.totalValue = _convertFromUSD(_priceOracle, collateralDebtData.totalValueUSD, underlying); // U:[CM-22,23] } - function _calcQuotedCollateral(address creditAccount, uint256 enabledTokensMask, IPriceOracleV2 _priceOracle) + /// @notice Gathers all data on the Credit Account's quoted tokens and quota interest + /// @param creditAccount Credit Account to return quoted token data for + /// @param enabledTokensMask Current mask of enabled tokens + /// @param _poolQuotaKeeper The PoolQuotaKeeper contract storing the quota and quota interest data + /// @return quotaTokens An array of address of quoted tokens on the Credit Account + /// @return outstandingQuotaInterest Quota interest that has not been saved in the Credit Manager + /// @return quotas Current quotas on quoted tokens, in the same order as quoted tokens + /// @return lts Current lts of quoted tokens, in the same order as quoted tokens + /// @return _quotedTokensMask The mask of enabled quoted tokens on the account + function _getQuotedTokensData(address creditAccount, uint256 enabledTokensMask, address _poolQuotaKeeper) internal view - returns (uint256 totalValueUSD, uint256 twvUSD, uint256 quotaInterest, address[] memory tokens) + returns ( + address[] memory quotaTokens, + uint256 outstandingQuotaInterest, + uint256[] memory quotas, + uint16[] memory lts, + uint256 _quotedTokensMask + ) { - uint256[] memory lts; - (tokens, lts) = _getQuotedTokens({enabledTokensMask: enabledTokensMask, withLTs: true}); + uint256 _maxEnabledTokens = maxEnabledTokens; // U:[CM-24] + _quotedTokensMask = quotedTokensMask; // U:[CM-24] - if (tokens.length > 0) { - /// If credit account has any connected token - then check that - (totalValueUSD, twvUSD, quotaInterest) = - poolQuotaKeeper().computeQuotedCollateralUSD(creditAccount, address(_priceOracle), tokens, lts); // F: [CMQ-8] - } + uint256 quotedMask = enabledTokensMask & _quotedTokensMask; // U:[CM-24] - quotaInterest += creditAccountInfo[creditAccount].cumulativeQuotaInterest - 1; // F: [CMQ-8] - } + // If there are not quoted tokens on the account, then zero-length arrays are returned + // This is desirable, as it makes it simple to check whether there are any quoted tokens + if (quotedMask != 0) { + quotaTokens = new address[](_maxEnabledTokens); // U:[CM-24] + quotas = new uint256[](_maxEnabledTokens); // U:[CM-24] + lts = new uint16[](_maxEnabledTokens); // U:[CM-24] - function _calcNotQuotedCollateral( - address creditAccount, - uint256 enabledTokensMask, - uint256 enoughCollateralUSD, - uint256[] memory collateralHints, - IPriceOracleV2 _priceOracle - ) internal view returns (uint256 tokensToDisable, uint256 totalValueUSD, uint256 twvUSD) { - uint256 tokenMask; - uint256 len = collateralHints.length; - bool nonZeroBalance; + uint256 j; + unchecked { + for (uint256 tokenMask = 2; tokenMask <= quotedMask; tokenMask <<= 1) { + if (j == _maxEnabledTokens) { + revert TooManyEnabledTokensException(); // U:[CM-24] + } - uint256 checkedTokenMask = supportsQuotas ? enabledTokensMask.disable(quotedTokenMask) : enabledTokensMask; + if (quotedMask & tokenMask != 0) { + address token; // U:[CM-24] + (token, lts[j]) = _collateralTokensByMask({tokenMask: tokenMask, calcLT: true}); // U:[CM-24] - if (enoughCollateralUSD != type(uint256).max) { - enoughCollateralUSD *= PERCENTAGE_FACTOR; - } + quotaTokens[j] = token; // U:[CM-24] - unchecked { - // TODO: add test that we check all values and it's always reachable - for (uint256 i; checkedTokenMask != 0; ++i) { - // TODO: add check for super long collateralnhints and for double masks - tokenMask = (i < len) ? collateralHints[i] : 1 << (i - len); // F: [CM-68] - - // CASE enabledTokensMask & tokenMask == 0 F:[CM-38] - if (checkedTokenMask & tokenMask != 0) { - (totalValueUSD, twvUSD, nonZeroBalance) = - _calcOneNonQuotedTokenCollateral(_priceOracle, tokenMask, creditAccount, totalValueUSD, twvUSD); - - // Collateral calculations are only done if there is a non-zero balance - if (nonZeroBalance) { - // Full collateral check evaluates a Credit Account's health factor lazily; - // Once the TWV computed thus far exceeds the debt, the check is considered - // successful, and the function returns without evaluating any further collateral - if (twvUSD >= enoughCollateralUSD) { - break; - } - // Zero-balance tokens are disabled; this is done by flipping the - // bit in enabledTokensMask, which is then written into storage at the - // very end, to avoid redundant storage writes - } else { - tokensToDisable |= tokenMask; // F:[CM-39] + uint256 outstandingInterestDelta; + (quotas[j], outstandingInterestDelta) = + IPoolQuotaKeeper(_poolQuotaKeeper).getQuotaAndOutstandingInterest(creditAccount, token); // U:[CM-24] + + /// Quota interest is equal to quota * APY * time. Since quota is a uint96, this is unlikely to overflow in any realistic scenario. + outstandingQuotaInterest += outstandingInterestDelta; // U:[CM-24] + + ++j; // U:[CM-24] } } - - checkedTokenMask &= (~tokenMask); } } - - twvUSD /= PERCENTAGE_FACTOR; - } - - function _calcOneNonQuotedTokenCollateral( - IPriceOracleV2 _priceOracle, - uint256 tokenMask, - address creditAccount, - uint256 _totalValueUSD, - uint256 _twvUSDx10K - ) internal view returns (uint256 totalValueUSD, uint256 twvUSDx10K, bool nonZeroBalance) { - (address token, uint16 liquidationThreshold) = collateralTokensByMask(tokenMask); - uint256 balance = IERC20(token)._balanceOf(creditAccount); - - // Collateral calculations are only done if there is a non-zero balance - if (balance > 1) { - uint256 balanceUSD = _convertToUSD(_priceOracle, balance, token); - totalValueUSD = _totalValueUSD + balanceUSD; - twvUSDx10K = _twvUSDx10K + balanceUSD * liquidationThreshold; - - nonZeroBalance = true; - } } // // QUOTAS MANAGEMENT // - /// @dev Returns the array of quoted tokens that are enabled on the account - function getQuotedTokens(address creditAccount) public view returns (address[] memory tokens) { - (tokens,) = _getQuotedTokens({enabledTokensMask: enabledTokensMaskOf(creditAccount), withLTs: false}); + /// @notice Updates credit account's quotas for multiple tokens + /// @param creditAccount Address of credit account + /// @param token Address of quoted token + /// @param quotaChange Change in quota in SIGNED format + function updateQuota(address creditAccount, address token, int96 quotaChange) + external + override + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] + returns (uint256 tokensToEnable, uint256 tokensToDisable) + { + /// The PoolQuotaKeeper returns the interest to be cached (quota interest is computed dynamically, + /// so the cumulative index inside PQK needs to be updated before setting the new quota value). + /// PQK also reports whether the quota was changed from zero to non-zero and vice versa, in order to + /// safely enable and disable quoted tokens + (uint256 caInterestChange, bool enable, bool disable) = IPoolQuotaKeeper(poolQuotaKeeper()).updateQuota({ + creditAccount: creditAccount, + token: token, + quotaChange: quotaChange + }); // U:[CM-25] // I: [CMQ-3] + + if (enable) { + tokensToEnable = getTokenMaskOrRevert(token); // U:[CM-25] + } else if (disable) { + tokensToDisable = getTokenMaskOrRevert(token); // U:[CM-25] + } + + creditAccountInfo[creditAccount].cumulativeQuotaInterest += caInterestChange; // U:[CM-25] // I: [CMQ-3] } - function _getQuotedTokens(uint256 enabledTokensMask, bool withLTs) - internal - view - returns (address[] memory tokens, uint256[] memory lts) + /// + /// WITHDRAWALS + /// + + /// @notice Schedules a delayed withdrawal of an asset from the account. + /// @dev Withdrawals in Gearbox V3 are generally delayed for safety, and an intermediate WithdrawalManager contract + /// is used to store funds pending a withdrawal. When the withdrawal matures, a corresponding `claimWithdrawals` function + /// can be used to receive them outside the Gearbox system. + /// @param creditAccount Credit Account to schedule a withdrawal for + /// @param token Token to withdraw + /// @param amount Amount to withdraw + function scheduleWithdrawal(address creditAccount, address token, uint256 amount) + external + override + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] + returns (uint256 tokensToDisable) { - uint256 quotedMask = enabledTokensMask & quotedTokenMask; + uint256 tokenMask = getTokenMaskOrRevert({token: token}); // U:[CM-26] - if (quotedMask > 0) { - tokens = new address[](maxAllowedEnabledTokenLength ); - lts = new uint256[](maxAllowedEnabledTokenLength ); + // If the configured delay is zero, then sending funds to the WithdrawalManager can be skipped + // and they can be sent directly to the user + if (IWithdrawalManager(withdrawalManager).delay() == 0) { + address borrower = getBorrowerOrRevert({creditAccount: creditAccount}); + _safeTokenTransfer({ + creditAccount: creditAccount, + token: token, + to: borrower, + amount: amount, + convertToETH: false + }); + } else { + uint256 delivered = ICreditAccount(creditAccount).transferDeliveredBalanceControl({ + token: token, + to: withdrawalManager, + amount: amount + }); - uint256 j; + IWithdrawalManager(withdrawalManager).addScheduledWithdrawal({ + creditAccount: creditAccount, + token: token, + amount: delivered, + tokenIndex: tokenMask.calcIndex() + }); - unchecked { - for (uint256 tokenMask = 2; tokenMask <= quotedMask; tokenMask <<= 1) { - if (quotedMask & tokenMask != 0) { - (tokens[j], lts[j]) = _collateralTokensByMask({tokenMask: tokenMask, calcLT: withLTs}); - ++j; - } - } - } + // WITHDRAWAL_FLAG is enabled on the account to efficiently determine + // whether the account has pending withdrawals in the future + _enableFlag({creditAccount: creditAccount, flag: WITHDRAWAL_FLAG}); + } + + if (IERC20Helper.balanceOf({token: token, holder: creditAccount}) <= 1) { + tokensToDisable = tokenMask; } } - /// @dev Updates credit account's quotas for multiple tokens - /// @param creditAccount Address of credit account - function updateQuota(address creditAccount, address token, int96 quotaChange) + /// @notice Resolves pending withdrawals, with logic dependent on the passed action. + /// @param creditAccount Credit Account to claim withdrawals for + /// @param to Address to claim withdrawals to + /// @param action Action to perform: + /// * CLAIM - claims mature withdrawals to `to`, leaving immature withdrawals as-is + /// * CANCEL - claims mature withdrawals and returns immature withdrawals to the credit account + /// * FORCE_CLAIM - claims all pending withdrawals, regardless of maturity + /// * FORCE_CANCEL - returns all pending withdrawals to the Credit Account regardless of maturity + function claimWithdrawals(address creditAccount, address to, ClaimAction action) external override - creditFacadeOnly // F: [CMQ-3] - returns (uint256 tokensToEnable, uint256 tokensToDisable) + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] + returns (uint256 tokensToEnable) { - (uint256 caInterestChange, bool enable, bool disable) = - poolQuotaKeeper().updateQuota(creditAccount, token, quotaChange); // F: [CMQ-3] + if (_hasWithdrawals(creditAccount)) { + bool hasScheduled; - if (enable) { - tokensToEnable = getTokenMaskOrRevert(token); - } else if (disable) { - tokensToDisable = getTokenMaskOrRevert(token); + (hasScheduled, tokensToEnable) = + IWithdrawalManager(withdrawalManager).claimScheduledWithdrawals(creditAccount, to, action); + if (!hasScheduled) { + // WITHDRAWAL_FLAG is disabled when there are no more pending withdrawals + _disableFlag(creditAccount, WITHDRAWAL_FLAG); + } } + } + + /// @notice Computes the value of cancellable withdrawals to add to Credit Account value + /// @param _priceOracle Price Oracle to compute the value of withdrawn assets + /// @param creditAccount Credit Account to compute value for + /// @param isForceCancel Whether to cancel all withdrawals or only immature ones + function _addCancellableWithdrawalsValue(address _priceOracle, address creditAccount, bool isForceCancel) + internal + view + returns (uint256 totalValueUSD) + { + (address token1, uint256 amount1, address token2, uint256 amount2) = + IWithdrawalManager(withdrawalManager).cancellableScheduledWithdrawals(creditAccount, isForceCancel); + + if (amount1 != 0) { + totalValueUSD = _convertToUSD({_priceOracle: _priceOracle, amountInToken: amount1, token: token1}); + } + if (amount2 != 0) { + totalValueUSD += _convertToUSD({_priceOracle: _priceOracle, amountInToken: amount2, token: token2}); + } + } + + /// @notice Revokes allowances for specified spender/token pairs + /// @dev When used with an older account factory, the Credit Manager may receive + /// an account with existing allowances. If the user is not comfortable with + /// these allowances, they can revoke them. + /// @param creditAccount Credit Account to revoke allowances for + /// @param revocations Spender/token pairs to revoke allowances for + function revokeAdapterAllowances(address creditAccount, RevocationPair[] calldata revocations) + external + override + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] + { + uint256 numRevocations = revocations.length; + unchecked { + for (uint256 i; i < numRevocations; ++i) { + address spender = revocations[i].spender; + address token = revocations[i].token; - creditAccountInfo[creditAccount].cumulativeQuotaInterest += caInterestChange; // F: [CMQ-3] + if (spender == address(0) || token == address(0)) { + revert ZeroAddressException(); + } + uint256 allowance = IERC20(token).allowance(creditAccount, spender); + /// It checks that token is in collateral token list in _approveSpender function + if (allowance > 1) { + _approveSpender({creditAccount: creditAccount, token: token, spender: spender, amount: 0}); + } + } + } } // - // INTERNAL HELPERS + // TRANSFER HELPERS // - /// @dev Transfers all enabled assets from a Credit Account to the "to" address + /// @notice Transfers all enabled assets from a Credit Account to the "to" address /// @param creditAccount Credit Account to transfer assets from /// @param to Recipient address - /// @param convertWETH Whether WETH must be converted to ETH before sending - /// @param enabledTokensMask A bit mask encoding enabled tokens. All of the tokens included + /// @param convertToETH Whether WETH must be converted to ETH before sending + /// @param tokensToTransferMask A bit mask encoding tokens to be transfered. All of the tokens included /// in the mask will be transferred. If any tokens need to be skipped, they must be /// excluded from the mask beforehand. - function _transferAssetsTo(address creditAccount, address to, bool convertWETH, uint256 enabledTokensMask) + function _batchTokensTransfer(address creditAccount, address to, bool convertToETH, uint256 tokensToTransferMask) internal { - // Since underlying should have been transferred to "to" before this function is called - // (if there is a surplus), its tokenMask of 1 is skipped - - // Since enabledTokensMask encodes all enabled tokens as 1, - // tokenMask > enabledTokensMask is equivalent to the last 1 bit being passed - // The loop can be ended at this point + // Since tokensToTransferMask encodes all enabled tokens as 1, tokenMask > enabledTokensMask is equivalent + // to the last 1 bit being passed. The loop can be ended at this point unchecked { - for (uint256 tokenMask = 2; tokenMask <= enabledTokensMask; tokenMask = tokenMask << 1) { - // enabledTokensMask & tokenMask == tokenMask when the token is enabled, - // and 0 otherwise - if (enabledTokensMask & tokenMask != 0) { + for (uint256 tokenMask = 1; tokenMask <= tokensToTransferMask; tokenMask = tokenMask << 1) { + // enabledTokensMask & tokenMask == tokenMask when the token is enabled, and 0 otherwise + if (tokensToTransferMask & tokenMask != 0) { address token = getTokenByMask(tokenMask); // F:[CM-44] - uint256 amount = IERC20(token)._balanceOf(creditAccount); // F:[CM-44] + uint256 amount = IERC20Helper.balanceOf({token: token, holder: creditAccount}); // F:[CM-44] if (amount > 1) { - // 1 is subtracted from amount to leave a non-zero value - // in the balance mapping, optimizing future writes - // Since the amount is checked to be more than 1, - // the block can be marked as unchecked - - // F:[CM-44] - _safeTokenTransfer(creditAccount, token, to, amount - 1, convertWETH); // F:[CM-44] + // 1 is subtracted from amount to leave a non-zero value in the balance mapping, optimizing future writes + // Since the amount is checked to be more than 1, the block can be marked as unchecked + _safeTokenTransfer({ + creditAccount: creditAccount, + token: token, + to: to, + amount: amount - 1, + convertToETH: convertToETH + }); // F:[CM-44] } } } - // The loop iterates by moving 1 bit to the left, - // which corresponds to moving on to the next token - // F:[CM-44] } } - /// @dev Requests the Credit Account to transfer a token to another address - /// Able to unwrap WETH before sending, if requested + /// @notice Requests the Credit Account to transfer a token to another address. If a token transfer + /// fails, the token will be transferred to WithdrawalManager, where the `to` address can + /// withdraw it from to any address. /// @param creditAccount Address of the sender Credit Account /// @param token Address of the token /// @param to Recipient address @@ -879,45 +1124,32 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard function _safeTokenTransfer(address creditAccount, address token, address to, uint256 amount, bool convertToETH) internal { - if (convertToETH && token == wethAddress) { - ICreditAccount(creditAccount).safeTransfer(token, wethGateway, amount); // F:[CM-45] - IWETHGateway(wethGateway).depositFor(to, amount); // F:[CM-45] + if (convertToETH && token == weth) { + ICreditAccount(creditAccount).transfer({token: token, to: wethGateway, amount: amount}); // F:[CM-45] + IWETHGateway(wethGateway).depositFor({to: to, amount: amount}); // F:[CM-45] } else { - try ICreditAccount(creditAccount).safeTransfer(token, to, amount) { // F:[CM-45] + // In case a token transfer fails (e.g., borrower getting blacklisted by USDC), the token will be sent + // to WithdrawalManager + try ICreditAccount(creditAccount).safeTransfer({token: token, to: to, amount: amount}) { + // F:[CM-45] } catch { - it(to == pool) revert; // TODO: add exception - uint256 delivered = ICreditAccount(creditAccount).safeTransferDeliveredBalanceControl( - token, address(withdrawalManager), amount - ); - withdrawalManager.addImmediateWithdrawal(to, token, delivered); + uint256 delivered = ICreditAccount(creditAccount).transferDeliveredBalanceControl({ + token: token, + to: withdrawalManager, + amount: amount + }); + IWithdrawalManager(withdrawalManager).addImmediateWithdrawal({token: token, to: to, amount: delivered}); } } } - function _checkEnabledTokenLength(uint256 enabledTokensMask) internal view { - uint256 totalTokensEnabled = enabledTokensMask.calcEnabledTokens(); - if (totalTokensEnabled > maxAllowedEnabledTokenLength) { - revert TooManyEnabledTokensException(); - } - } - // // GETTERS // - /// @dev Returns the collateral token at requested index and its liquidation threshold - /// @param id The index of token to return - function collateralTokens(uint256 id) public view returns (address token, uint16 liquidationThreshold) { - // Collateral tokens are stored under their masks rather than - // indicies, so this is simply a convenience function that wraps - // the getter by mask - return collateralTokensByMask(1 << id); - } - - /// @dev Returns the collateral token with requested mask and its liquidationThreshold - /// @param tokenMask Token mask corresponding to the token - // TODO: naming! + /// @notice Returns the collateral token with requested mask and its liquidationThreshold + /// @param tokenMask Token mask corresponding to the token function collateralTokensByMask(uint256 tokenMask) public view @@ -927,11 +1159,27 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard return _collateralTokensByMask({tokenMask: tokenMask, calcLT: true}); } + /// @notice Returns the mask for the provided token + /// @param token Token to returns the mask for + function getTokenMaskOrRevert(address token) public view override returns (uint256 tokenMask) { + tokenMask = (token == underlying) ? 1 : tokenMasksMapInternal[token]; + if (tokenMask == 0) revert TokenNotAllowedException(); + } + + /// @notice Returns the collateral token with requested mask + /// @param tokenMask Token mask corresponding to the token function getTokenByMask(uint256 tokenMask) public view override returns (address token) { (token,) = _collateralTokensByMask({tokenMask: tokenMask, calcLT: false}); } - /// @dev Returns the collateral token with requested mask and its liquidationThreshold + /// @notice Returns the liquidation threshold for the provided token + /// @param token Token to retrieve the LT for + function liquidationThresholds(address token) public view override returns (uint16 lt) { + uint256 tokenMask = getTokenMaskOrRevert(token); + (, lt) = _collateralTokensByMask({tokenMask: tokenMask, calcLT: true}); // F:[CM-47] + } + + /// @notice Returns the collateral token with requested mask and its liquidationThreshold /// @param tokenMask Token mask corresponding to the token function _collateralTokensByMask(uint256 tokenMask, bool calcLT) internal @@ -947,78 +1195,14 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard token = tokenData.getTokenOrRevert(); if (calcLT) { + // The logic to calculate a ramping LT is isolated to the `CreditLogic` library. + // See `CreditLogic.getLiquidationThreshold()` for details. liquidationThreshold = tokenData.getLiquidationThreshold(); } } } - /// @dev Returns the address of a borrower's Credit Account, or reverts if there is none. - /// @param borrower Borrower's address - function getBorrowerOrRevert(address creditAccount) public view override returns (address borrower) { - borrower = creditAccountInfo[creditAccount].borrower; // F:[CM-48] - if (borrower == address(0)) revert CreditAccountNotExistsException(); // F:[CM-48] - } - - /// @dev IMPLEMENTATION: calcCreditAccountAccruedInterest - /// @param creditAccount Address of the Credit Account - /// @param quotaInterest Total quota premiums accrued, computed elsewhere - /// @return debt The debt principal - /// @return accruedInterest Accrued interest - /// @return accruedFees Accrued interest and protocol fees - function _calcCreditAccountAccruedInterest(address creditAccount, uint256 quotaInterest) - internal - view - returns (uint256 debt, uint256 accruedInterest, uint256 accruedFees) - { - uint256 cumulativeIndexLastUpdate; - uint256 cumulativeIndexNow; - (debt, cumulativeIndexLastUpdate, cumulativeIndexNow) = _getCreditAccountParameters(creditAccount); // F:[CM-49] - - // Interest is never stored and is always computed dynamically - // as the difference between the current cumulative index of the pool - // and the cumulative index recorded in the Credit Account - accruedInterest = - CreditLogic.calcAccruedInterest(debt, cumulativeIndexLastUpdate, cumulativeIndexNow) + quotaInterest; // F:[CM-49] - - // Fees are computed as a percentage of interest - accruedFees = accruedInterest * feeInterest / PERCENTAGE_FACTOR; // F: [CM-49] - } - - /// @dev Returns the parameters of the Credit Account required to calculate debt - /// @param creditAccount Address of the Credit Account - /// @return debt Debt principal amount - /// @return cumulativeIndexLastUpdate The cumulative index value used to calculate - /// interest in conjunction with current pool index. Not necessarily the index - /// value at the time of account opening, since it can be updated by manageDebt. - /// @return cumulativeIndexNow Current cumulative index of the pool - function _getCreditAccountParameters(address creditAccount) - internal - view - returns (uint256 debt, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow) - { - debt = creditAccountInfo[creditAccount].debt; // F:[CM-49,50] - cumulativeIndexLastUpdate = creditAccountInfo[creditAccount].cumulativeIndexLastUpdate; // F:[CM-49,50] - cumulativeIndexNow = IPoolService(pool).calcLinearCumulative_RAY(); // F:[CM-49,50] - } - - /// @dev Returns the liquidation threshold for the provided token - /// @param token Token to retrieve the LT for - function liquidationThresholds(address token) public view override returns (uint16 lt) { - // Underlying is a special case and its LT is stored separately - if (token == underlying) return ltUnderlying; // F:[CM-47] - - uint256 tokenMask = getTokenMaskOrRevert(token); - (, lt) = _collateralTokensByMask({tokenMask: tokenMask, calcLT: true}); // F:[CM-47] - } - - /// @dev Returns the mask for the provided token - /// @param token Token to returns the mask for - function getTokenMaskOrRevert(address token) public view override returns (uint256 tokenMask) { - tokenMask = (token == underlying) ? 1 : tokenMasksMapInternal[token]; - if (tokenMask == 0) revert TokenNotAllowedException(); - } - - /// @dev Returns the fee parameters of the Credit Manager + /// @notice Returns the fee parameters of the Credit Manager /// @return _feeInterest Percentage of interest taken by the protocol as profit /// @return _feeLiquidation Percentage of account value taken by the protocol as profit /// during unhealthy account liquidations @@ -1047,62 +1231,155 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard _liquidationDiscountExpired = liquidationDiscountExpired; // F:[CM-51] } - /// @dev Address of the connected pool - /// @notice [DEPRECATED]: use pool() instead. + /// @notice Address of the connected pool + /// @dev [DEPRECATED]: use pool() instead. function poolService() external view returns (address) { return pool; } - function poolQuotaKeeper() public view returns (IPoolQuotaKeeper) { - return IPoolQuotaKeeper(IPool4626(pool).poolQuotaKeeper()); + /// @notice Adress of the connected PoolQuotaKeeper + /// @dev PoolQuotaKeeper is a contract that manages token quota parameters + /// and computes quota interest. Since quota interest is paid directly to the pool, + /// this contract is responsible for aligning quota interest values between the + /// pool, gauge and the Credit Manager + function poolQuotaKeeper() public view returns (address) { + return IPoolV3(pool).poolQuotaKeeper(); + } + + /// + /// CREDIT ACCOUNT INFO + /// + + /// @notice Returns the owner of the provided CA, or reverts if there is none + /// @param creditAccount Credit Account to get the borrower for + function getBorrowerOrRevert(address creditAccount) public view override returns (address borrower) { + borrower = creditAccountInfo[creditAccount].borrower; // F:[CM-48] + if (borrower == address(0)) revert CreditAccountNotExistsException(); // F:[CM-48] + } + + /// @notice Returns the mask containing the account's enabled tokens + /// @param creditAccount Credit Account to get the mask for + function enabledTokensMaskOf(address creditAccount) public view override returns (uint256) { + return creditAccountInfo[creditAccount].enabledTokensMask; + } + + /// @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 + /// @param creditAccount Account to get the mask for + function flagsOf(address creditAccount) external view override returns (uint16) { + return creditAccountInfo[creditAccount].flags; + } + + /// @notice Sets a flag for a Credit Account + /// @param creditAccount Account to set a flag for + /// @param flag Flag to set + /// @param value The new flag value + function setFlagFor(address creditAccount, uint16 flag, bool value) + external + override + nonReentrant // U:[CM-5] + creditFacadeOnly // U:[CM-2] + { + if (value) { + _enableFlag(creditAccount, flag); + } else { + _disableFlag(creditAccount, flag); + } + } + + /// @notice Sets the flag in the CA's flag mask to 1 + function _enableFlag(address creditAccount, uint16 flag) internal { + creditAccountInfo[creditAccount].flags |= flag; + } + + /// @notice Sets the flag in the CA's flag mask to 0 + function _disableFlag(address creditAccount, uint16 flag) internal { + creditAccountInfo[creditAccount].flags &= ~flag; + } + + /// @notice Efficiently checks whether the CA has pending withdrawals using the flag + function _hasWithdrawals(address creditAccount) internal view returns (bool) { + return creditAccountInfo[creditAccount].flags & WITHDRAWAL_FLAG != 0; + } + + /// @notice Checks quantity of enabled tokens and saves the mask to creditAccountInfo + function _saveEnabledTokensMask(address creditAccount, uint256 enabledTokensMask) internal { + if (enabledTokensMask.calcEnabledTokens() > maxEnabledTokens) { + revert TooManyEnabledTokensException(); + } + creditAccountInfo[creditAccount].enabledTokensMask = enabledTokensMask; + } + + /// + /// FEE TOKEN SUPPORT + /// + + /// @notice Returns the amount that needs to be transferred to get exactly `amount` delivered + /// @dev Can be overriden in inheritor contracts to support tokens with fees (such as USDT) + function _amountWithFee(uint256 amount) internal view virtual returns (uint256) { + return amount; + } + + /// @notice Returns the amount that will be delivered after `amount` is tranferred + /// @dev Can be overriden in inheritor contracts to support tokens with fees (such as USDT) + function _amountMinusFee(uint256 amount) internal view virtual returns (uint256) { + return amount; + } + + /// + /// CREDIT ACCOUNTS + /// + + /// @notice Returns the full set of currently active Credit Accounts + function creditAccounts() external view returns (address[] memory) { + return creditAccountsSet.values(); } // // CONFIGURATION // - // The following function change vital Credit Manager parameters + // The following functions change vital Credit Manager parameters // and can only be called by the Credit Configurator // - /// @dev Adds a token to the list of collateral tokens + /// @notice Adds a token to the list of collateral tokens /// @param token Address of the token to add function addToken(address token) external - creditConfiguratorOnly // F:[CM-4] + creditConfiguratorOnly // U:[CM-4] { _addToken(token); // F:[CM-52] } - /// @dev IMPLEMENTATION: addToken + /// @notice IMPLEMENTATION: addToken /// @param token Address of the token to add function _addToken(address token) internal { // Checks that the token is not already known (has an associated token mask) - if (tokenMasksMapInternal[token] > 0) { + if (tokenMasksMapInternal[token] != 0) { revert TokenAlreadyAddedException(); } // F:[CM-52] // Checks that there aren't too many tokens - // Since token masks are 248 bit numbers with each bit corresponding to 1 token, - // only at most 248 are supported - if (collateralTokensCount >= 248) revert TooManyTokensException(); // F:[CM-52] + // Since token masks are 255 bit numbers with each bit corresponding to 1 token, + // only at most 255 are supported + if (collateralTokensCount >= 255) revert TooManyTokensException(); // F:[CM-52] // The tokenMask of a token is a bit mask with 1 at position corresponding to its index // (i.e. 2 ** index or 1 << index) uint256 tokenMask = 1 << collateralTokensCount; tokenMasksMapInternal[token] = tokenMask; // F:[CM-53] - collateralTokensData[tokenMask] = CollateralTokenData({ - token: token, - ltInitial: 0, - ltFinal: 0, - timestampRampStart: type(uint40).max, - rampDuration: 0 - }); // F:[CM-47] + collateralTokensData[tokenMask].token = token; + collateralTokensData[tokenMask].timestampRampStart = type(uint40).max; // F:[CM-47] - collateralTokensCount++; // F:[CM-47] + unchecked { + ++collateralTokensCount; // F:[CM-47] + } } - /// @dev Sets fees and premiums + /// @notice Sets fees and premiums /// @param _feeInterest Percentage of interest taken by the protocol as profit /// @param _feeLiquidation Percentage of account value taken by the protocol as profit /// during unhealthy account liquidations @@ -1112,7 +1389,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard /// during expired account liquidations /// @param _liquidationDiscountExpired Multiplier that reduces the effective totalValue during expired account liquidations, /// allowing the liquidator to take the unaccounted for remainder as premium. Equal to (1 - liquidationPremiumExpired) - function setParams( + function setFees( uint16 _feeInterest, uint16 _feeLiquidation, uint16 _liquidationDiscount, @@ -1120,7 +1397,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard uint16 _liquidationDiscountExpired ) external - creditConfiguratorOnly // F:[CM-4] + creditConfiguratorOnly // U:[CM-4] { feeInterest = _feeInterest; // F:[CM-51] feeLiquidation = _feeLiquidation; // F:[CM-51] @@ -1129,13 +1406,10 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard liquidationDiscountExpired = _liquidationDiscountExpired; // F:[CM-51] } - // - // CONFIGURATION - // - - /// @dev Sets ramping parameters for a token's liquidation threshold - /// @notice Ramping parameters allow to decrease the LT gradually over a period of time + /// @notice Sets ramping parameters for a token's liquidation threshold + /// @dev Ramping parameters allow to decrease the LT gradually over a period of time /// which gives users/bots time to react and adjust their position for the new LT + /// @dev A static LT can be forced by setting ltInitial to desired LT and setting timestampRampStart to unit40.max /// @param token The collateral token to set the LT for /// @param finalLT The final LT after ramping /// @param timestampRampStart Timestamp when the LT starts ramping @@ -1146,7 +1420,10 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard uint16 finalLT, uint40 timestampRampStart, uint24 rampDuration - ) external creditConfiguratorOnly { + ) + external + creditConfiguratorOnly // U:[CM-4] + { if (token == underlying) { ltUnderlying = initialLT; // F:[CM-47] } else { @@ -1160,34 +1437,37 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard } } - /// @dev Sets the limited token mask - /// @param _quotedTokenMask The new mask - /// @notice Limited tokens are counted as collateral not based on their balances, - /// but instead based on their quotas set in the poolQuotaKeeper contract + /// @notice Sets the quoted token mask + /// @param _quotedTokensMask The new mask + /// @dev Quoted tokens are counted as collateral not only based on their balances, + /// but also on their quotas set in thePpoolQuotaKeeper contract /// Tokens in the mask also incur additional interest based on their quotas - function setQuotedMask(uint256 _quotedTokenMask) + function setQuotedMask(uint256 _quotedTokensMask) external - creditConfiguratorOnly // F: [CMQ-2] + creditConfiguratorOnly // U:[CM-4] { - quotedTokenMask = _quotedTokenMask; // F: [CMQ-2] + quotedTokensMask = _quotedTokensMask; // I: [CMQ-2] } - /// @dev Sets the maximal number of enabled tokens on a single Credit Account. - /// @param newMaxEnabledTokens The new enabled token limit. - function setMaxEnabledTokens(uint8 newMaxEnabledTokens) + /// @notice Sets the maximal number of enabled tokens on a single Credit Account. + /// @param _maxEnabledTokens The new enabled token quantity limit. + function setMaxEnabledTokens(uint8 _maxEnabledTokens) external - creditConfiguratorOnly // F: [CM-4] + creditConfiguratorOnly // U: [CM-4] { - maxAllowedEnabledTokenLength = newMaxEnabledTokens; // F: [CC-37] + maxEnabledTokens = _maxEnabledTokens; // F: [CC-37] } - /// @dev Sets the link between an adapter and its corresponding targetContract + /// @notice Sets the link between an adapter and its corresponding targetContract /// @param adapter Address of the adapter to be used to access the target contract /// @param targetContract A 3rd-party contract for which the adapter is set - /// @notice The function can be called with (adapter, address(0)) and (address(0), targetContract) + /// @dev The function can be called with (adapter, address(0)) and (address(0), targetContract) /// to disallow a particular target or adapter, since this would set values in respective /// mappings to address(0). - function setContractAllowance(address adapter, address targetContract) external creditConfiguratorOnly { + function setContractAllowance(address adapter, address targetContract) + external + creditConfiguratorOnly // U: [CM-4] + { if (targetContract == address(this) || adapter == address(this)) { revert TargetContractNotAllowedException(); } // F:[CC-13] @@ -1200,181 +1480,87 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuard } } - /// @dev Sets the Credit Facade + /// @notice Sets the Credit Facade /// @param _creditFacade Address of the new Credit Facade function setCreditFacade(address _creditFacade) external - creditConfiguratorOnly // F:[CM-4] + creditConfiguratorOnly // U: [CM-4] { creditFacade = _creditFacade; } - /// @dev Sets the Price Oracle + /// @notice Sets the Price Oracle /// @param _priceOracle Address of the new Price Oracle function setPriceOracle(address _priceOracle) external - creditConfiguratorOnly // F:[CM-4] + creditConfiguratorOnly // U: [CM-4] { - priceOracle = IPriceOracleV2(_priceOracle); + priceOracle = _priceOracle; } - /// @dev Sets a new Credit Configurator + /// @notice Sets a new Credit Configurator /// @param _creditConfigurator Address of the new Credit Configurator function setCreditConfigurator(address _creditConfigurator) external - creditConfiguratorOnly // F:[CM-4] + creditConfiguratorOnly // U: [CM-4] { creditConfigurator = _creditConfigurator; // F:[CM-58] emit SetCreditConfigurator(_creditConfigurator); // F:[CM-58] } - /// ----------- /// - /// WITHDRAWALS /// - /// ----------- /// - - /// @inheritdoc ICreditManagerV3 - function scheduleWithdrawal(address creditAccount, address token, uint256 amount) - external - override - creditFacadeOnly - returns (uint256 tokensToDisable) - { - uint256 tokenMask = getTokenMaskOrRevert(token); - - uint256 delivered = - ICreditAccount(creditAccount).safeTransferDeliveredBalanceControl(token, address(withdrawalManager), amount); - - withdrawalManager.addScheduledWithdrawal(creditAccount, token, delivered, tokenMask.calcIndex()); - - /// @dev enables withdrawal flag - creditAccountInfo[creditAccount].flags |= WITHDRAWAL_FLAG; - - // We need to disable empty tokens in case they could be forbidden, to finally eliminate them - if (IERC20(token)._balanceOf(creditAccount) <= 1) { - tokensToDisable = tokenMask; - } - } - - /// @inheritdoc ICreditManagerV3 - function claimWithdrawals(address creditAccount, address to, ClaimAction action) - external - override - creditFacadeOnly - returns (uint256 tokensToEnable) - { - if (_hasWithdrawals(creditAccount)) { - bool hasScheduled; - (hasScheduled, tokensToEnable) = withdrawalManager.claimScheduledWithdrawals(creditAccount, to, action); - if (!hasScheduled) { - /// @dev disables withdrawal flag - creditAccountInfo[creditAccount].flags &= ~WITHDRAWAL_FLAG; - } - } - } - - function _hasWithdrawals(address creditAccount) internal view returns (bool) { - return creditAccountInfo[creditAccount].flags & WITHDRAWAL_FLAG != 0; - } - - function _calcCancellableWithdrawalsValue(IPriceOracleV2 _priceOracle, address creditAccount, bool isForceCancel) - internal - view - returns (uint256 withdrawalsValueUSD) - { - (address token1, uint256 amount1, address token2, uint256 amount2) = - withdrawalManager.cancellableScheduledWithdrawals(creditAccount, isForceCancel); - - if (amount1 > 0) withdrawalsValueUSD += _convertToUSD(_priceOracle, amount1, token1); - if (amount2 > 0) withdrawalsValueUSD += _convertToUSD(_priceOracle, amount2, token2); - } - - /// @notice Revokes allowances for specified spender/token pairs - /// @param revocations Spender/token pairs to revoke allowances for - function revokeAdapterAllowances(address creditAccount, RevocationPair[] calldata revocations) - external - override - creditFacadeOnly - { - uint256 numRevocations = revocations.length; - unchecked { - for (uint256 i; i < numRevocations; ++i) { - address spender = revocations[i].spender; - address token = revocations[i].token; - - if (spender == address(0) || token == address(0)) { - revert ZeroAddressException(); - } - uint256 allowance = IERC20(token).allowance(creditAccount, spender); - /// It checks that token is in collateral token list in _approveSpender function - if (allowance > 1) _approveSpender(token, spender, creditAccount, 0); - } - } - } - /// - function setCreditAccountForExternalCall(address creditAccount) external override creditFacadeOnly { - _externalCallCreditAccount = creditAccount; - } + /// EXTERNAL CALLS HELPERS + /// - function externalCallCreditAccountOrRevert() public view override returns (address creditAccount) { - creditAccount = _externalCallCreditAccount; - if (creditAccount == address(1)) revert ExternalCallCreditAccountNotSetException(); - } + // + // POOL HELPERS + // - function enabledTokensMaskOf(address creditAccount) public view override returns (uint256) { - return uint256(creditAccountInfo[creditAccount].enabledTokensMask); + /// @notice Returns the current pool cumulative index + function _poolCumulativeIndexNow() internal view returns (uint256) { + return IPoolBase(pool).calcLinearCumulative_RAY(); } - function flagsOf(address creditAccount) external view override returns (uint16) { - return creditAccountInfo[creditAccount].flags; + /// @notice Notifies the pool that there was a debt repayment + /// @param debt Amount of debt principal repaid + /// @param profit Amount of treasury earned (if any) + /// @param loss Amount of loss incurred (if any) + function _poolRepayCreditAccount(uint256 debt, uint256 profit, uint256 loss) internal { + IPoolBase(pool).repayCreditAccount(debt, profit, loss); } - function setFlagFor(address creditAccount, uint16 flag, bool value) external override creditFacadeOnly { - if (value) { - creditAccountInfo[creditAccount].flags |= flag; - } else { - creditAccountInfo[creditAccount].flags &= ~flag; - } + /// @notice Requests the pool to lend funds to a Credit Account + /// @param amount Amount of funds to lend + /// @param creditAccount Address of the Credit Account to lend to + function _poolLendCreditAccount(uint256 amount, address creditAccount) internal { + IPoolBase(pool).lendCreditAccount(amount, creditAccount); // F:[CM-20] } - function _saveEnabledTokensMask(address creditAccount, uint256 enabledTokensMask) internal { - if (enabledTokensMask > type(uint248).max) { - revert IncorrectParameterException(); - } - _checkEnabledTokenLength(enabledTokensMask); - creditAccountInfo[creditAccount].enabledTokensMask = uint248(enabledTokensMask); - } + // + // PRICE ORACLE + // - function _convertFromUSD(IPriceOracleV2 _priceOracle, uint256 amountInUSD, address token) + /// @notice Returns the value of a token amount in USD + /// @param _priceOracle Price oracle to query for token value + /// @param amountInToken Amount of token to convert + /// @param token Token to convert + function _convertToUSD(address _priceOracle, uint256 amountInToken, address token) internal view - returns (uint256 amountInToken) + returns (uint256 amountInUSD) { - amountInToken = _priceOracle.convertFromUSD(amountInUSD, token); + amountInUSD = IPriceOracleV2(_priceOracle).convertToUSD(amountInToken, token); } - function _convertToUSD(IPriceOracleV2 _priceOracle, uint256 amountInToken, address token) + /// @notice Returns amount of token after converting from a provided USD amount + /// @param _priceOracle Price oracle to query for token value + /// @param amountInUSD USD amount to convert + /// @param token Token to convert to + function _convertFromUSD(address _priceOracle, uint256 amountInUSD, address token) internal view - returns (uint256 amountInUSD) + returns (uint256 amountInToken) { - amountInUSD = _priceOracle.convertToUSD(amountInToken, token); - } - - /// - /// FEE TOKEN SUPPORT - /// - - function _amountWithFee(uint256 amount) internal view virtual returns (uint256) { - return amount; - } - - function _amountMinusFee(uint256 amount) internal view virtual returns (uint256) { - return amount; - } - - // CREDIT ACCOUNTS - function creditAccounts() external view returns (address[] memory) { - return creditAccountsSet.values(); + amountInToken = IPriceOracleV2(_priceOracle).convertFromUSD(amountInUSD, token); } } diff --git a/contracts/credit/CreditManagerV3_USDT.sol b/contracts/credit/CreditManagerV3_USDT.sol new file mode 100644 index 00000000..c54d41ea --- /dev/null +++ b/contracts/credit/CreditManagerV3_USDT.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +import {CreditManagerV3} from "./CreditManagerV3.sol"; +import {USDT_Transfer} from "../traits/USDT_Transfer.sol"; +import {IPoolBase} from "../interfaces/IPoolV3.sol"; +/// @title Credit Manager + +contract CreditManagerV3_USDT is CreditManagerV3, USDT_Transfer { + constructor(address _addressProvider, address _pool) + CreditManagerV3(_addressProvider, _pool) + USDT_Transfer(IPoolBase(_pool).underlyingToken()) + {} + + function _amountWithFee(uint256 amount) internal view override returns (uint256) { + return _amountUSDTWithFee(amount); + } + + function _amountMinusFee(uint256 amount) internal view override returns (uint256) { + return _amountUSDTMinusFee(amount); + } +} diff --git a/contracts/factories/CreditManagerFactory.sol b/contracts/factories/CreditManagerFactory.sol index 0f3812bc..3f86ac43 100644 --- a/contracts/factories/CreditManagerFactory.sol +++ b/contracts/factories/CreditManagerFactory.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox. Generalized leverage protocol that allows to take leverage and then use it across other DeFi protocols and platforms in a composable way. // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import "@openzeppelin/contracts/utils/Create2.sol"; @@ -17,8 +17,8 @@ contract CreditManagerFactory { CreditFacadeV3 public creditFacade; CreditConfigurator public creditConfigurator; - constructor(address _pool, CreditManagerOpts memory opts, bytes32 salt) { - creditManager = new CreditManagerV3(_pool, opts.withdrawalManager); + constructor(address _ap, address _pool, CreditManagerOpts memory opts, bytes32 salt) { + creditManager = new CreditManagerV3(_ap, _pool); creditFacade = new CreditFacadeV3( address(creditManager), opts.degenNFT, diff --git a/contracts/interfaces/IAccountFactory.sol b/contracts/interfaces/IAccountFactory.sol index bb104085..938d45c9 100644 --- a/contracts/interfaces/IAccountFactory.sol +++ b/contracts/interfaces/IAccountFactory.sol @@ -5,11 +5,6 @@ pragma solidity ^0.8.17; import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; -enum TakeAccountAction { - TAKE_USED_ONE, - DEPLOY_NEW_ONE -} - interface IAccountFactoryEvents { /// @dev Emits when a new Credit Account is created event DeployCreditAccount(address indexed creditAccount); diff --git a/contracts/interfaces/IAccountFactoryV3.sol b/contracts/interfaces/IAccountFactoryV3.sol new file mode 100644 index 00000000..ecdb6433 --- /dev/null +++ b/contracts/interfaces/IAccountFactoryV3.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2023 +pragma solidity ^0.8.17; + +import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; + +interface IAccountFactoryV3Events { + /// @notice Emitted when new credit account is deployed + event DeployCreditAccount(address indexed creditAccount, address indexed creditManager); + + /// @notice Emitted when credit account is taken by the credit manager + event TakeCreditAccount(address indexed creditAccount, address indexed creditManager); + + /// @notice Emitted when used credit account is returned to the queue + event ReturnCreditAccount(address indexed creditAccount, address indexed creditManager); + + /// @notice Emitted when new credit manager is added to the factory + event AddCreditManager(address indexed creditManager, address masterCreditAccount); +} + +/// @title Account factory V3 interface +interface IAccountFactoryV3 is IAccountFactoryV3Events, IVersion { + /// @notice Delay after which returned credit accounts can be reused + function delay() external view returns (uint40); + + /// @notice Provides a reusable credit account from the queue to the credit manager. + /// If there are no accounts that can be reused in the queue, deploys a new one. + /// @return creditAccount Address of the provided credit account + /// @dev Parameters are ignored and only kept for backward compatibility + /// @custom:expects Credit manager sets account's borrower to non-zero address after calling this function + function takeCreditAccount(uint256, uint256) external returns (address creditAccount); + + /// @notice Returns a used credit account to the queue + /// @param creditAccount Address of the returned credit account + /// @custom:expects Credit account is connected to the calling credit manager + /// @custom:expects Credit manager sets account's borrower to zero-address before calling this function + function returnCreditAccount(address creditAccount) external; + + /// @notice Adds a credit manager to the factory and deploys the master credit account for it + /// @param creditManager Credit manager address + function addCreditManager(address creditManager) external; + + /// @notice Executes function call from the account to the target contract with provided data, + /// can only be called by configurator when account is not in use by anyone. + /// Allows to rescue funds that were accidentally left on the account upon closure. + /// @param creditAccount Credit account to execute the call from + /// @param target Contract to call + /// @param data Data to call the target contract with + function rescue(address creditAccount, address target, bytes calldata data) external; +} diff --git a/contracts/interfaces/IAdapter.sol b/contracts/interfaces/IAdapter.sol index eda073a0..1bd4841d 100644 --- a/contracts/interfaces/IAdapter.sol +++ b/contracts/interfaces/IAdapter.sol @@ -3,20 +3,18 @@ // (c) Gearbox Holdings, 2023 pragma solidity ^0.8.17; -import {IAddressProvider} from "@gearbox-protocol/core-v2/contracts/interfaces/IAddressProvider.sol"; import {AdapterType} from "@gearbox-protocol/core-v2/contracts/interfaces/adapters/IAdapter.sol"; -import {ICreditManagerV3} from "./ICreditManagerV3.sol"; /// @title Adapter interface interface IAdapter { - /// @notice Credit Manager the adapter is connected to - function creditManager() external view returns (ICreditManagerV3); + /// @notice Credit manager the adapter is connected to + function creditManager() external view returns (address); /// @notice Address of the contract the adapter is interacting with function targetContract() external view returns (address); - /// @notice Address provider - function addressProvider() external view returns (IAddressProvider); + /// @notice Address provider contract + function addressProvider() external view returns (address); /// @notice Adapter type function _gearboxAdapterType() external pure returns (AdapterType); diff --git a/contracts/interfaces/IAddressProviderV3.sol b/contracts/interfaces/IAddressProviderV3.sol new file mode 100644 index 00000000..7908490e --- /dev/null +++ b/contracts/interfaces/IAddressProviderV3.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; + +uint256 constant NO_VERSION_CONTROL = 0; +// Repositories & services +bytes32 constant AP_CONTRACTS_REGISTER = "CONTRACTS_REGISTER"; +bytes32 constant AP_ACL = "ACL"; +bytes32 constant AP_PRICE_ORACLE = "PRICE_ORACLE"; +bytes32 constant AP_ACCOUNT_FACTORY = "ACCOUNT_FACTORY"; +bytes32 constant AP_DATA_COMPRESSOR = "DATA_COMPRESSOR"; +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"; + +interface IAddressProviderEvents { + /// @dev Emits when an address is set for a contract role + event AddressSet(bytes32 indexed service, address indexed newAddress, uint256 indexed version); +} + +interface IAddressProviderV3 is IAddressProviderEvents, IVersion { + function getAddressOrRevert(bytes32 key, uint256 _version) external view returns (address result); + + function setAddress(bytes32 key, address value, bool saveVersion) external; +} diff --git a/contracts/interfaces/IBotList.sol b/contracts/interfaces/IBotList.sol index 4bd233a5..dfa77624 100644 --- a/contracts/interfaces/IBotList.sol +++ b/contracts/interfaces/IBotList.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; @@ -14,28 +14,75 @@ struct BotFunding { interface IBotListEvents { /// @dev Emits when a borrower enables or disables a bot for their account - event ApproveBot(address indexed borrower, address indexed bot, uint256 permissions); + event SetBotPermissions( + address indexed creditAccount, + address indexed bot, + uint256 permissions, + uint72 fundingAmount, + uint72 weeklyFundingAllowance + ); /// @dev Emits when a bot is forbidden system-wide event BotForbiddenStatusChanged(address indexed bot, bool status); - /// @dev Emits when the amount of remaining funds for a bot is changed by the user - event ChangeBotFunding(address indexed payer, address indexed bot, uint72 newRemainingFunds); + /// @dev Emits when the user changes the amount of funds in his bot wallet + event ChangeFunding(address indexed payer, uint256 newRemainingFunds); /// @dev Emits when the allowed weekly amount of bot's spending is changed by the user event ChangeBotWeeklyAllowance(address indexed payer, address indexed bot, uint72 newWeeklyAllowance); - /// @dev Emits when the bot pull payment for performed services - event PullBotPayment(address indexed payer, address indexed bot, uint72 paymentAmount, uint72 daoFeeAmount); + /// @dev Emits when the bot is paid for performed services + event PayBot( + address indexed payer, + address indexed creditAccount, + address indexed bot, + uint72 paymentAmount, + uint72 daoFeeAmount + ); /// @dev Emits when the DAO sets a new fee on bot payments event SetBotDAOFee(uint16 newFee); + + /// @dev Emits when all bot permissions for a Credit Account are erased + event EraseBots(address creditAccount); + + /// @dev Emits when a new Credit Manager is approved in BotList + event CreditManagerAdded(address indexed creditManager); + + /// @dev Emits when a Credit Manager is removed from BotList + event CreditManagerRemoved(address indexed creditManager); } /// @title IBotList interface IBotList is IBotListEvents, IVersion { /// @dev Sets approval from msg.sender to bot - function setBotPermissions(address bot, uint192 permissions) external; + function setBotPermissions( + address creditAccount, + address bot, + uint192 permissions, + uint72 fundingAmount, + uint72 weeklyFundingAllowance + ) external returns (uint256 remainingBots); + + /// @dev Removes permissions and funding for all bots with non-zero permissions for a credit account + /// @param creditAccount Credit Account to erase permissions for + function eraseAllBotPermissions(address creditAccount) external; + + /// @dev Adds funds to the borrower's bot payment wallet + function addFunding() external payable; + + /// @dev Removes funds from the borrower's bot payment wallet + function removeFunding(uint256 amount) external; + + /// @dev Takes payment for performed services from the user's balance and sends to the bot + /// @param payer Address to charge + /// @param creditAccount Address of the credit account paid for + /// @param bot Address of the bot to pay + /// @param paymentAmount Amount to pay + function payBot(address payer, address creditAccount, address bot, uint72 paymentAmount) external; + + /// @dev Returns all active bots currently on the account + function getActiveBots(address creditAccount) external view returns (address[] memory); /// @dev Returns whether the bot is approved by the borrower function botPermissions(address borrower, address bot) external view returns (uint192); @@ -43,22 +90,9 @@ interface IBotList is IBotListEvents, IVersion { /// @dev Returns whether the bot is forbidden by the borrower function forbiddenBot(address bot) external view returns (bool); - /// @dev Adds funds to user's balance for a particular bot. The entire sent value in ETH is added - /// @param bot Address of the bot to fund - function increaseBotFunding(address bot) external payable; - - /// @dev Removes funds from the user's balance for a particular bot. The funds are sent to the user. - /// @param bot Address of the bot to remove funds from - /// @param decreaseAmount Amount to remove - function decreaseBotFunding(address bot, uint72 decreaseAmount) external; - - /// @dev Sets the amount that can be pull by the bot per week - /// @param bot Address of the bot to set allowance for - /// @param allowanceAmount Amount of weekly allowance - function setWeeklyBotAllowance(address bot, uint72 allowanceAmount) external; - - /// @dev Takes payment from the user to the bot for performed services - /// @param payer Address of the paying user - /// @param paymentAmount Amount to pull - function pullPayment(address payer, uint72 paymentAmount) external; + /// @dev Returns information about bot permissions + function getBotStatus(address bot, address creditAccount) + external + view + returns (uint192 permissions, bool forbidden); } diff --git a/contracts/interfaces/IControllerTimelock.sol b/contracts/interfaces/IControllerTimelock.sol index a600c0df..d86ab13c 100644 --- a/contracts/interfaces/IControllerTimelock.sol +++ b/contracts/interfaces/IControllerTimelock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; struct QueuedTransactionData { bool queued; @@ -87,6 +87,7 @@ interface IControllerTimelock is IControllerTimelockErrors, IControllerTimelockE address creditManager, address token, uint16 liquidationThresholdFinal, + uint40 rampStart, uint24 rampDuration ) external; diff --git a/contracts/interfaces/ICreditAccount.sol b/contracts/interfaces/ICreditAccount.sol index fa369ba8..9b02ba4c 100644 --- a/contracts/interfaces/ICreditAccount.sol +++ b/contracts/interfaces/ICreditAccount.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; diff --git a/contracts/interfaces/ICreditAccountV3.sol b/contracts/interfaces/ICreditAccountV3.sol new file mode 100644 index 00000000..d5b6c2ab --- /dev/null +++ b/contracts/interfaces/ICreditAccountV3.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2023 +pragma solidity ^0.8.17; + +import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; + +/// @title Credit account V3 interface +interface ICreditAccountV3 is IVersion { + /// @notice Account factory this account was deployed with + function factory() external view returns (address); + + /// @notice Credit manager this account is connected to + function creditManager() external view returns (address); + + /// @notice Transfers tokens from the credit account, can only be called by the credit manager + /// @param token Token to transfer + /// @param to Transfer recipient + /// @param amount Amount to transfer + function safeTransfer(address token, address to, uint256 amount) external; + + /// @notice Executes function call from the account to the target contract with provided data, + /// can only be called by the credit manager + /// @param target Contract to call + /// @param data Data to call the target contract with + /// @return result Call result + function execute(address target, bytes memory data) external returns (bytes memory result); + + /// @notice Executes function call from the account to the target contract with provided data, + /// can only be called by the factory. + /// Allows to rescue funds that were accidentally left on the account upon closure. + /// @param target Contract to call + /// @param data Data to call the target contract with + function rescue(address target, bytes memory data) external; +} diff --git a/contracts/interfaces/ICreditConfiguratorV3.sol b/contracts/interfaces/ICreditConfiguratorV3.sol index 3d5e5ec2..545f69fe 100644 --- a/contracts/interfaces/ICreditConfiguratorV3.sol +++ b/contracts/interfaces/ICreditConfiguratorV3.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IAddressProvider} from "@gearbox-protocol/core-v2/contracts/interfaces/IAddressProvider.sol"; import {CreditManagerV3} from "../credit/CreditManagerV3.sol"; @@ -146,7 +146,12 @@ interface ICreditConfigurator is ICreditConfiguratorEvents, IVersion { /// @param token Token to ramp LT for /// @param liquidationThresholdFinal Liquidation threshold after ramping /// @param rampDuration Duration of ramping - function rampLiquidationThreshold(address token, uint16 liquidationThresholdFinal, uint24 rampDuration) external; + function rampLiquidationThreshold( + address token, + uint16 liquidationThresholdFinal, + uint40 rampStart, + uint24 rampDuration + ) external; /// @dev Allow a known collateral token if it was forbidden before. /// @param token Address of collateral token @@ -169,11 +174,6 @@ interface ICreditConfigurator is ICreditConfiguratorEvents, IVersion { /// @param targetContract Address of a contract to be forbidden function forbidContract(address targetContract) external; - /// @dev Forbids adapter (and only the adapter - the target contract is not affected) - /// @param adapter Address of adapter to disable - /// @notice Used to clean up orphaned adapters - function forbidAdapter(address adapter) external; - /// @dev Sets borrowed amount limits in Credit Facade /// @param _minBorrowedAmount Minimum borrowed amount /// @param _maxBorrowedAmount Maximum borrowed amount @@ -195,7 +195,7 @@ interface ICreditConfigurator is ICreditConfiguratorEvents, IVersion { /// @dev Upgrades the price oracle in the Credit Manager, taking the address /// from the address provider - function setPriceOracle() external; + function setPriceOracle(uint256 version) external; /// @dev Upgrades the Credit Facade corresponding to the Credit Manager /// @param _creditFacade address of the new CreditFacadeV3 @@ -242,15 +242,16 @@ interface ICreditConfigurator is ICreditConfiguratorEvents, IVersion { function resetCumulativeLoss() external; /// @dev Sets the bot list contract - /// @param botList The address of the new bot list - function setBotList(address botList) external; + /// @param version The version of the new bot list contract + /// The contract address is retrieved from addressProvider + function setBotList(uint256 version) external; // // GETTERS // /// @dev Address provider (needed for upgrading the Price Oracle) - function addressProvider() external view returns (IAddressProvider); + function addressProvider() external view returns (address); /// @dev Returns the Credit Facade currently connected to the Credit Manager function creditFacade() external view returns (CreditFacadeV3); @@ -263,4 +264,10 @@ interface ICreditConfigurator is ICreditConfiguratorEvents, IVersion { /// @dev Returns all allowed contracts function allowedContracts() external view returns (address[] memory); + + /// @dev Returns all emergency liquidators + function emergencyLiquidators() external view returns (address[] memory); + + /// @dev Returns all forbidden tokens + function forbiddenTokens() external view returns (address[] memory); } diff --git a/contracts/interfaces/ICreditFacade.sol b/contracts/interfaces/ICreditFacade.sol index f3a2c610..06d5c968 100644 --- a/contracts/interfaces/ICreditFacade.sol +++ b/contracts/interfaces/ICreditFacade.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {MultiCall} from "@gearbox-protocol/core-v2/contracts/libraries/MultiCall.sol"; import {ICreditManagerV3} from "./ICreditManagerV3.sol"; @@ -10,6 +10,20 @@ import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion. import {ClosureAction} from "../interfaces/ICreditManagerV3.sol"; import "./ICreditFacadeMulticall.sol"; +struct DebtLimits { + /// @dev Minimal borrowed amount per credit account + uint128 minDebt; + /// @dev Maximum aborrowed amount per credit account + uint128 maxDebt; +} + +struct CumulativeLossParams { + /// @dev Current cumulative loss from all bad debt liquidations + uint128 currentCumulativeLoss; + /// @dev Max cumulative loss accrued before the system is paused + uint128 maxCumulativeLoss; +} + struct FullCheckParams { uint256[] collateralHints; uint16 minHealthFactor; @@ -73,13 +87,10 @@ interface ICreditFacade is ICreditFacadeEvents, IVersion { /// @param calls The array of MultiCall structs encoding the required operations. Generally must have /// at least a call to addCollateral, as otherwise the health check at the end will fail. /// @param referralCode Referral code which is used for potential rewards. 0 if no referral code provided - function openCreditAccount( - uint256 debt, - address onBehalfOf, - MultiCall[] calldata calls, - bool deployNewAccount, - uint16 referralCode - ) external payable returns (address creditAccount); + function openCreditAccount(uint256 debt, address onBehalfOf, MultiCall[] calldata calls, uint16 referralCode) + external + payable + returns (address creditAccount); /// @dev Runs a batch of transactions within a multicall and closes the account /// - Wraps ETH to WETH and sends it msg.sender if value > 0 @@ -90,18 +101,18 @@ interface ICreditFacade is ICreditFacadeEvents, IVersion { /// from the Credit Account and proceeds. If not, tries to transfer the shortfall from msg.sender. /// + Transfers all enabled assets with non-zero balances to the "to" address, unless they are marked /// to be skipped in skipTokenMask - /// + If convertWETH is true, converts WETH into ETH before sending to the recipient + /// + If convertToETH is true, converts WETH into ETH before sending to the recipient /// - Emits a CloseCreditAccount event /// /// @param to Address to send funds to during account closing /// @param skipTokenMask Uint-encoded bit mask where 1's mark tokens that shouldn't be transferred - /// @param convertWETH If true, converts WETH into ETH before sending to "to" + /// @param convertToETH If true, converts WETH into ETH before sending to "to" /// @param calls The array of MultiCall structs encoding the operations to execute before closing the account. function closeCreditAccount( address creditAccount, address to, uint256 skipTokenMask, - bool convertWETH, + bool convertToETH, MultiCall[] calldata calls ) external payable; @@ -122,18 +133,18 @@ interface ICreditFacade is ICreditFacadeEvents, IVersion { /// + Transfers all enabled assets with non-zero balances to the "to" address, unless they are marked /// to be skipped in skipTokenMask. If the liquidator is confident that all assets were converted /// during the multicall, they can set the mask to uint256.max - 1, to only transfer the underlying - /// + If convertWETH is true, converts WETH into ETH before sending + /// + If convertToETH is true, converts WETH into ETH before sending /// - Emits LiquidateCreditAccount event /// /// @param to Address to send funds to after liquidation /// @param skipTokenMask Uint-encoded bit mask where 1's mark tokens that shouldn't be transferred - /// @param convertWETH If true, converts WETH into ETH before sending to "to" + /// @param convertToETH If true, converts WETH into ETH before sending to "to" /// @param calls The array of MultiCall structs encoding the operations to execute before liquidating the account. function liquidateCreditAccount( address creditAccount, address to, uint256 skipTokenMask, - bool convertWETH, + bool convertToETH, MultiCall[] calldata calls ) external payable; @@ -176,6 +187,20 @@ interface ICreditFacade is ICreditFacadeEvents, IVersion { function claimWithdrawals(address creditAccount, address to) external; + /// @dev Sets permissions and funding parameters for a bot + /// @param creditAccount CA to set permissions for + /// @param bot Bot to set permissions for + /// @param permissions A bit mask of permissions + /// @param fundingAmount Total amount of ETH available to the bot for payments + /// @param weeklyFundingAllowance Amount of ETH available to the bot weekly + function setBotPermissions( + address creditAccount, + address bot, + uint192 permissions, + uint72 fundingAmount, + uint72 weeklyFundingAllowance + ) external; + // // GETTERS // @@ -184,7 +209,7 @@ interface ICreditFacade is ICreditFacadeEvents, IVersion { function forbiddenTokenMask() external view returns (uint256); /// @dev Returns the CreditManagerV3 connected to this Credit Facade - function creditManager() external view returns (ICreditManagerV3); + function creditManager() external view returns (address); /// @dev Returns true if 'from' is allowed to transfer Credit Accounts to 'to' /// @param from Sender address to check allowance for @@ -203,9 +228,6 @@ interface ICreditFacade is ICreditFacadeEvents, IVersion { /// @dev Address of the DegenNFT that gatekeeps account openings in whitelisted mode function degenNFT() external view returns (address); - /// @dev Address of the underlying asset - function underlying() external view returns (address); - /// @dev Maps addresses to their status as emergency liquidator. /// @notice Emergency liquidators are trusted addresses /// that are able to liquidate positions while the contracts are paused, diff --git a/contracts/interfaces/ICreditFacadeMulticall.sol b/contracts/interfaces/ICreditFacadeMulticall.sol index 5c36ce03..083b417f 100644 --- a/contracts/interfaces/ICreditFacadeMulticall.sol +++ b/contracts/interfaces/ICreditFacadeMulticall.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {Balance} from "@gearbox-protocol/core-v2/contracts/libraries/Balances.sol"; import {RevocationPair} from "./ICreditManagerV3.sol"; @@ -14,6 +14,7 @@ uint256 constant DISABLE_TOKEN_PERMISSION = 2 ** 4; uint256 constant WITHDRAW_PERMISSION = 2 ** 5; uint256 constant UPDATE_QUOTA_PERMISSION = 2 ** 6; uint256 constant REVOKE_ALLOWANCES_PERMISSION = 2 ** 7; +uint256 constant PAY_BOT_PERMISSION = 2 ** 8; uint256 constant EXTERNAL_CALLS_PERMISSION = 2 ** 16; uint256 constant ALL_CREDIT_FACADE_CALLS_PERMISSION = ADD_COLLATERAL_PERMISSION | INCREASE_DEBT_PERMISSION @@ -81,4 +82,8 @@ interface ICreditFacadeMulticall { function scheduleWithdrawal(address token, uint256 amount) external; function revokeAdapterAllowances(RevocationPair[] calldata revocations) external; + + function onDemandPriceUpdate(address token, bytes memory data) external; + + function payBot(uint72 paymentAmount) external; } diff --git a/contracts/interfaces/ICreditManagerV3.sol b/contracts/interfaces/ICreditManagerV3.sol index 0618e2b8..05f1f537 100644 --- a/contracts/interfaces/ICreditManagerV3.sol +++ b/contracts/interfaces/ICreditManagerV3.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IPriceOracleV2} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; import {IPoolQuotaKeeper} from "./IPoolQuotaKeeper.sol"; @@ -32,23 +32,33 @@ struct CreditAccountInfo { } enum CollateralCalcTask { + GENERIC_PARAMS, DEBT_ONLY, DEBT_COLLATERAL_WITHOUT_WITHDRAWALS, DEBT_COLLATERAL_CANCEL_WITHDRAWALS, - DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS + DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS, + /// + FULL_COLLATERAL_CHECK_LAZY } struct CollateralDebtData { uint256 debt; + uint256 cumulativeIndexNow; + uint256 cumulativeIndexLastUpdate; + uint256 cumulativeQuotaInterest; uint256 accruedInterest; uint256 accruedFees; + uint256 totalDebtUSD; uint256 totalValue; uint256 totalValueUSD; uint256 twvUSD; - uint16 hf; uint256 enabledTokensMask; + uint256 quotedTokensMask; address[] quotedTokens; - bool isLiquidatable; + uint16[] quotedLts; + uint256[] quotas; + /// + address _poolQuotaKeeper; } struct CollateralTokenData { @@ -83,7 +93,7 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { /// @dev Opens credit account and borrows funds from the pool. /// @param debt Amount to be borrowed by the Credit Account /// @param onBehalfOf The owner of the newly opened Credit Account - function openCreditAccount(uint256 debt, address onBehalfOf, bool deployNew) external returns (address); + function openCreditAccount(uint256 debt, address onBehalfOf) external returns (address); /// @dev Closes a Credit Account - covers both normal closure and liquidation /// - Checks whether the contract is paused, and, if so, if the payer is an emergency liquidator. @@ -101,7 +111,7 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { /// + if it is larger than amountToPool, then the pool is paid fully from funds on the Credit Account /// + else tries to transfer the shortfall from the payer - either the borrower during closure, or liquidator during liquidation /// - Send assets to the "to" address, as long as they are not included into skipTokenMask - /// - If convertWETH is true, the function converts WETH into ETH before sending + /// - If convertToETH is true, the function converts WETH into ETH before sending /// - Returns the Credit Account back to factory /// /// @param creditAccount Credit account address @@ -109,7 +119,7 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { /// @param payer Address which would be charged if credit account has not enough funds to cover amountToPool /// @param to Address to which the leftover funds will be sent /// @param skipTokensMask Tokenmask contains 1 for tokens which needed to be skipped for sending - /// @param convertWETH If true converts WETH to ETH + /// @param convertToETH If true converts WETH to ETH function closeCreditAccount( address creditAccount, ClosureAction closureAction, @@ -117,7 +127,7 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { address payer, address to, uint256 skipTokensMask, - bool convertWETH + bool convertToETH ) external returns (uint256 remainingFunds, uint256 loss); /// @dev Manages debt size for borrower: @@ -205,10 +215,6 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { /// the bit at the position equal to token's index to 1 function enabledTokensMaskOf(address creditAccount) external view returns (uint256); - /// @dev Returns the collateral token at requested index and its liquidation threshold - /// @param id The index of token to return - function collateralTokens(uint256 id) external view returns (address token, uint16 liquidationThreshold); - /// @dev Returns the collateral token with requested mask and its liquidationThreshold /// @param tokenMask Token mask corresponding to the token function collateralTokensByMask(uint256 tokenMask) @@ -216,8 +222,8 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { view returns (address token, uint16 liquidationThreshold); - /// @dev Returns the array of quoted tokens that are enabled on the account - function getQuotedTokens(address creditAccount) external view returns (address[] memory tokens); + // /// @dev Returns the array of quoted tokens that are enabled on the account + // function getQuotedTokens(address creditAccount) external view returns (address[] memory tokens); /// @dev Total number of known collateral tokens. function collateralTokensCount() external view returns (uint8); @@ -227,7 +233,7 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { function getTokenMaskOrRevert(address token) external view returns (uint256); /// @dev Mask of tokens to apply quotas for - function quotedTokenMask() external view returns (uint256); + function quotedTokensMask() external view returns (uint256); /// @dev Maps allowed adapters to their respective target contracts. function adapterToContract(address adapter) external view returns (address); @@ -246,7 +252,7 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { function poolService() external view returns (address); /// @dev Returns the current pool quota keeper connected to the pool - function poolQuotaKeeper() external view returns (IPoolQuotaKeeper); + function poolQuotaKeeper() external view returns (address); /// @dev Whether the Credit Manager supports quotas function supportsQuotas() external view returns (bool); @@ -255,7 +261,7 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { function creditConfigurator() external view returns (address); /// @dev Address of WETH - function wethAddress() external view returns (address); + function weth() external view returns (address); /// @dev Address of WETHGateway function wethGateway() external view returns (address); @@ -265,7 +271,7 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { function liquidationThresholds(address token) external view returns (uint16); /// @dev The maximal number of enabled tokens on a single Credit Account - function maxAllowedEnabledTokenLength() external view returns (uint8); + function maxEnabledTokens() external view returns (uint8); /// @dev Returns the fee parameters of the Credit Manager /// @return feeInterest Percentage of interest taken by the protocol as profit @@ -288,19 +294,24 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { uint16 liquidationDiscountExpired ); + /// @dev Address of address provider + function addressProvider() external view returns (address); + /// @dev Address of the connected Credit Facade function creditFacade() external view returns (address); /// @dev Address of the connected Price Oracle - function priceOracle() external view returns (IPriceOracleV2); + function priceOracle() external view returns (address); function calcDebtAndCollateral(address creditAccount, CollateralCalcTask task) external view returns (CollateralDebtData memory collateralDebtData); + function isLiquidatable(address creditAccount, uint16 minHealthFactor) external view returns (bool); + /// @dev Withdrawal manager - function withdrawalManager() external view returns (IWithdrawalManager); + function withdrawalManager() external view returns (address); function scheduleWithdrawal(address creditAccount, address token, uint256 amount) external @@ -314,9 +325,9 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { /// @param revocations Spender/token pairs to revoke allowances for function revokeAdapterAllowances(address creditAccount, RevocationPair[] calldata revocations) external; - function setCreditAccountForExternalCall(address creditAccount) external; + function setActiveCreditAccount(address creditAccount) external; - function externalCallCreditAccountOrRevert() external view returns (address creditAccount); + function getActiveCreditAccountOrRevert() external view returns (address creditAccount); function getTokenByMask(uint256 tokenMask) external view returns (address token); diff --git a/contracts/interfaces/IExceptions.sol b/contracts/interfaces/IExceptions.sol index 7ce8562f..2cc7cccc 100644 --- a/contracts/interfaces/IExceptions.sol +++ b/contracts/interfaces/IExceptions.sol @@ -15,13 +15,18 @@ error NotImplementedException(); error IncorrectParameterException(); error RegisteredCreditManagerOnlyException(); + error RegisteredPoolOnlyException(); error WethPoolsOnlyException(); + error ReceiveIsNotAllowedException(); error IncompatibleCreditManagerException(); +/// @dev Reverts if address isn't found in address provider +error AddressNotFoundException(); + /// @dev Thrown on attempting to set an EOA as an important contract in the system error AddressIsNotContractException(address); @@ -38,6 +43,9 @@ error IncorrectTokenContractException(); /// correct price feed error IncorrectPriceFeedException(); +/// @dev Thrown on attempting to get a result for a token that does not have a price feed +error PriceFeedNotExistsException(); + /// /// ACCESS /// @@ -48,6 +56,9 @@ error CallerNotCreditAccountOwnerException(); /// @dev Thrown on attempting to call an access restricted function as a non-Configurator error CallerNotConfiguratorException(); +/// @dev Thrown on attempting to call an access-restructed function not as account factory +error CallerNotAccountFactoryException(); + /// @dev Thrown on attempting to call an access restricted function as a non-CreditManagerV3 error CallerNotCreditManagerException(); @@ -77,9 +88,6 @@ error CallerNotVoterException(); /// the connected Credit Facade, or an allowed adapter error CallerNotAdapterException(); -/// @dev Thrown if an access-restricted function is called by an address that is not withdrawal manager -error CallerNotWithdrawalManagerException(); - /// interface ICreditConfiguratorExceptions { /// @dev Thrown if the underlying's LT is set directly @@ -207,7 +215,7 @@ error BorrowingMoreU2ForbiddenException(); /// @dev Thrown on returning a value that violates the current bounds error ValueOutOfRangeException(); -// interface IPool4626Exceptions { +// interface IPoolV3Exceptions { error ExpectedLiquidityLimitException(); error CreditManagerCantBorrowException(); @@ -235,4 +243,13 @@ error NoFreeWithdrawalSlotsException(); error NoPermissionException(uint256 permission); -error ExternalCallCreditAccountNotSetException(); +error ActiveCreditAccountNotSetException(); + +/// @dev Thrown when attempting to set positive funding for a bot with 0 permissions +error PositiveFundingForInactiveBotException(); + +/// @dev Thrown when trying to deploy second master credit account for a credit manager +error MasterCreditAccountAlreadyDeployedException(); + +/// @dev Thrown when trying to rescue funds from a credit account that is currently in use +error CreditAccountIsInUseException(); diff --git a/contracts/interfaces/IGauge.sol b/contracts/interfaces/IGauge.sol index f0e4240c..29d7e664 100644 --- a/contracts/interfaces/IGauge.sol +++ b/contracts/interfaces/IGauge.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IGearStaking} from "./IGearStaking.sol"; import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; diff --git a/contracts/interfaces/IInterestRateModel.sol b/contracts/interfaces/IInterestRateModel.sol index 5ab49638..d27fcbf4 100644 --- a/contracts/interfaces/IInterestRateModel.sol +++ b/contracts/interfaces/IInterestRateModel.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; diff --git a/contracts/interfaces/ILPPriceFeed.sol b/contracts/interfaces/ILPPriceFeed.sol index 144565ba..99675cd0 100644 --- a/contracts/interfaces/ILPPriceFeed.sol +++ b/contracts/interfaces/ILPPriceFeed.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import {IPriceFeedType} from "./IPriceFeedType.sol"; diff --git a/contracts/interfaces/IPoolQuotaKeeper.sol b/contracts/interfaces/IPoolQuotaKeeper.sol index d122c845..4954a900 100644 --- a/contracts/interfaces/IPoolQuotaKeeper.sol +++ b/contracts/interfaces/IPoolQuotaKeeper.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; -import {IPool4626} from "./IPool4626.sol"; +import {IPoolV3} from "./IPoolV3.sol"; struct TokenQuotaParams { uint96 totalQuoted; @@ -56,9 +56,7 @@ interface IPoolQuotaKeeper is IPoolQuotaKeeperEvents, IVersion { /// @dev Computes the accrued quota interest and updates interest indexes /// @param creditAccount Address of the Credit Account to accrue interest for /// @param tokens Array of all active quoted tokens on the account - function accrueQuotaInterest(address creditAccount, address[] memory tokens) - external - returns (uint256 caQuotaInterestChange); + function accrueQuotaInterest(address creditAccount, address[] memory tokens) external; /// @dev Gauge management @@ -73,7 +71,7 @@ interface IPoolQuotaKeeper is IPoolQuotaKeeperEvents, IVersion { // /// @dev Returns the gauge address - function pool() external view returns (IPool4626); + function pool() external view returns (address); /// @dev Returns the gauge address function gauge() external view returns (address); @@ -97,16 +95,8 @@ interface IPoolQuotaKeeper is IPoolQuotaKeeperEvents, IVersion { returns (uint96 quota, uint192 cumulativeIndexLU); /// @dev Computes collateral value for quoted tokens on the account, as well as accrued quota interest - function computeQuotedCollateralUSD( - address creditAccount, - address _priceOracle, - address[] memory tokens, - uint256[] memory lts - ) external view returns (uint256 totalValue, uint256 twv, uint256 totalQuotaInterest); - - /// @dev Computes outstanding quota interest - function outstandingQuotaInterest(address creditAccount, address[] memory tokens) + function getQuotaAndOutstandingInterest(address creditAccount, address token) external view - returns (uint256 caQuotaInterestChange); + returns (uint256 quoted, uint256 outstandingInterest); } diff --git a/contracts/interfaces/IPool4626.sol b/contracts/interfaces/IPoolV3.sol similarity index 94% rename from contracts/interfaces/IPool4626.sol rename to contracts/interfaces/IPoolV3.sol index b1e37317..ccf42017 100644 --- a/contracts/interfaces/IPool4626.sol +++ b/contracts/interfaces/IPoolV3.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; pragma abicoder v1; import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; import {AddressProvider} from "@gearbox-protocol/core-v2/contracts/core/AddressProvider.sol"; import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; -interface IPool4626Events { +interface IPoolV3Events { /// @dev Emits on new liquidity being added to the pool event DepositWithReferral(address indexed sender, address indexed onBehalfOf, uint256 amount, uint16 referralCode); @@ -42,13 +42,7 @@ interface IPool4626Events { event SetWithdrawFee(uint256 fee); } -/// @title Pool 4626 -/// More: https://dev.gearbox.fi/developers/pool/abstractpoolservice -interface IPool4626 is IPool4626Events, IERC4626, IVersion { - function depositReferral(uint256 assets, address receiver, uint16 referralCode) external returns (uint256 shares); - - function burn(uint256 shares) external; - +interface IPoolBase { /// CREDIT MANAGERS FUNCTIONS /// @dev Lends pool funds to a Credit Account @@ -64,14 +58,8 @@ interface IPool4626 is IPool4626Events, IERC4626, IVersion { /// was already transferred function repayCreditAccount(uint256 borrowedAmount, uint256 profit, uint256 loss) external; - /// @dev Updates quota index - function changeQuotaRevenue(int128 _quotaRevenueChange) external; - - function updateQuotaRevenue(uint128 newQuotaRevenue) external; - - // - // GETTERS - // + /// @dev Address of the underlying + function underlyingToken() external view returns (address); /// @dev The same value like in total assets in ERC4626 standrt function expectedLiquidity() external view returns (uint256); @@ -84,6 +72,23 @@ interface IPool4626 is IPool4626Events, IERC4626, IVersion { /// @dev Current interest index, RAY format function calcLinearCumulative_RAY() external view returns (uint256); +} + +/// @title Pool 4626 +/// More: https://dev.gearbox.fi/developers/pool/abstractpoolservice +interface IPoolV3 is IPoolV3Events, IPoolBase, IERC4626, IVersion { + function depositReferral(uint256 assets, address receiver, uint16 referralCode) external returns (uint256 shares); + + function burn(uint256 shares) external; + + /// @dev Updates quota index + function changeQuotaRevenue(int128 _quotaRevenueChange) external; + + function updateQuotaRevenue(uint128 newQuotaRevenue) external; + + // + // GETTERS + // /// @dev Calculates the current borrow rate, RAY format function borrowRate() external view returns (uint256); @@ -91,9 +96,6 @@ interface IPool4626 is IPool4626Events, IERC4626, IVersion { /// @dev Total borrowed amount (includes principal only) function totalBorrowed() external view returns (uint256); - /// @dev Address of the underlying - function underlyingToken() external view returns (address); - /// @dev Addresses of all connected credit managers function creditManagers() external view returns (address[] memory); @@ -118,8 +120,8 @@ interface IPool4626 is IPool4626Events, IERC4626, IVersion { function totalBorrowedLimit() external view returns (uint256); /// @dev Address provider - function addressProvider() external view returns (AddressProvider); + function addressProvider() external view returns (address); // @dev Connects pool quota manager - function connectPoolQuotaManager(address) external; + function setPoolQuotaManager(address) external; } diff --git a/contracts/interfaces/IPriceFeedOnDemand.sol b/contracts/interfaces/IPriceFeedOnDemand.sol new file mode 100644 index 00000000..1d3b415e --- /dev/null +++ b/contracts/interfaces/IPriceFeedOnDemand.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +interface IPriceFeedOnDemand { + /// @dev Returns the number of decimals in the price feed's returned result + function decimals() external view returns (uint8); + + /// @dev Returns the price feed descriptiom + function description() external view returns (string memory); + + /// @dev Returns the price feed version + function version() external view returns (uint256); + + /// @dev Returns the latest price feed value + /// @notice Return type is according to Chainlink spec + function latestRoundData(address creditAccount) + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); + + function updatePrice(bytes calldata data) external; +} diff --git a/contracts/interfaces/IWETHGateway.sol b/contracts/interfaces/IWETHGateway.sol index b0a5f6b7..c8442c6f 100644 --- a/contracts/interfaces/IWETHGateway.sol +++ b/contracts/interfaces/IWETHGateway.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; interface IWETHGateway { /// @dev POOL V3: diff --git a/contracts/interfaces/IWithdrawalManager.sol b/contracts/interfaces/IWithdrawalManager.sol index 7411e43e..72030152 100644 --- a/contracts/interfaces/IWithdrawalManager.sol +++ b/contracts/interfaces/IWithdrawalManager.sol @@ -66,13 +66,9 @@ interface IWithdrawalManagerEvents { /// @notice Emitted when new scheduled withdrawal delay is set by configurator /// @param delay New delay for scheduled withdrawals event SetWithdrawalDelay(uint40 delay); - - /// @notice Emitted when new credit manager status is set by configurator - /// @param creditManager Credit manager for which the status is set - /// @param status New status of the credit manager - event SetCreditManagerStatus(address indexed creditManager, bool status); } +/// @title Withdrawal manager interface interface IWithdrawalManager is IWithdrawalManagerEvents, IVersion { /// --------------------- /// /// IMMEDIATE WITHDRAWALS /// @@ -82,13 +78,13 @@ interface IWithdrawalManager is IWithdrawalManagerEvents, IVersion { function immediateWithdrawals(address account, address token) external view returns (uint256); /// @notice Adds new immediate withdrawal for the account - /// @param account Account to add immediate withdrawal for /// @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 account, address token, uint256 amount) external; + function addImmediateWithdrawal(address token, address to, uint256 amount) external; - /// @notice Claims `msg.sender`'s immediate withdrawal + /// @notice Claims caller's immediate withdrawal /// @param token Token to claim /// @param to Token recipient function claimImmediateWithdrawal(address token, address to) external; @@ -138,15 +134,7 @@ interface IWithdrawalManager is IWithdrawalManagerEvents, IVersion { /// CONFIGURATION /// /// ------------- /// - /// @notice Whether given address is a supported credit manager - function creditManagerStatus(address) external view returns (bool); - /// @notice Sets delay for scheduled withdrawals, only affects new withdrawal requests /// @param delay New delay for scheduled withdrawals function setWithdrawalDelay(uint40 delay) external; - - /// @notice Sets status for the credit manager - /// @param creditManager Credit manager to set the status for - /// @param status New status of the credit manager - function setCreditManagerStatus(address creditManager, bool status) external; } diff --git a/contracts/interfaces/external/IUSDT.sol b/contracts/interfaces/external/IUSDT.sol deleted file mode 100644 index 9610e234..00000000 --- a/contracts/interfaces/external/IUSDT.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.8.4; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -interface IUSDT is IERC20 { - function basisPointsRate() external view returns (uint256); - - function maximumFee() external view returns (uint256); -} diff --git a/contracts/libraries/BalancesLogic.sol b/contracts/libraries/BalancesLogic.sol new file mode 100644 index 00000000..53ad99c4 --- /dev/null +++ b/contracts/libraries/BalancesLogic.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +import {CollateralDebtData, CollateralTokenData} from "../interfaces/ICreditManagerV3.sol"; +import {IERC20Helper} from "./IERC20Helper.sol"; +import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; +import {SECONDS_PER_YEAR} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; +import "../interfaces/IExceptions.sol"; + +import {BitMask} from "./BitMask.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {RAY} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; +import {Balance} from "@gearbox-protocol/core-v2/contracts/libraries/Balances.sol"; + +uint256 constant INDEX_PRECISION = 10 ** 9; + +import "forge-std/console.sol"; + +/// @title Credit Logic Library +library BalancesLogic { + using BitMask for uint256; + + /// @param creditAccount Credit Account to compute balances for + function storeBalances(address creditAccount, Balance[] memory desired) + internal + view + returns (Balance[] memory expected) + { + // Retrieves the balance list from calldata + + expected = desired; // F:[FA-45] + uint256 len = expected.length; // F:[FA-45] + + for (uint256 i = 0; i < len;) { + expected[i].balance += IERC20Helper.balanceOf(expected[i].token, creditAccount); // F:[FA-45] + unchecked { + ++i; + } + } + } + + /// @dev Compares current balances to previously saved expected balances. + /// Reverts if at least one balance is lower than expected + /// @param creditAccount Credit Account to check + /// @param expected Expected balances after all operations + + function compareBalances(address creditAccount, Balance[] memory expected) internal view { + uint256 len = expected.length; // F:[FA-45] + unchecked { + for (uint256 i = 0; i < len; ++i) { + if (IERC20Helper.balanceOf(expected[i].token, creditAccount) < expected[i].balance) { + revert BalanceLessThanMinimumDesiredException(expected[i].token); + } // F:[FA-45] + } + } + } + + function storeForbiddenBalances( + address creditAccount, + uint256 enabledTokensMask, + uint256 forbiddenTokenMask, + function (uint256) view returns (address) getTokenByMaskFn + ) internal view returns (uint256[] memory forbiddenBalances) { + uint256 forbiddenTokensOnAccount = enabledTokensMask & forbiddenTokenMask; + + if (forbiddenTokensOnAccount != 0) { + forbiddenBalances = new uint256[](forbiddenTokensOnAccount.calcEnabledTokens()); + unchecked { + uint256 i; + for (uint256 tokenMask = 1; tokenMask < forbiddenTokensOnAccount; tokenMask <<= 1) { + if (forbiddenTokensOnAccount & tokenMask != 0) { + address token = getTokenByMaskFn(tokenMask); + forbiddenBalances[i] = IERC20Helper.balanceOf(token, creditAccount); + ++i; + } + } + } + } + } + + function checkForbiddenBalances( + address creditAccount, + uint256 enabledTokensMaskBefore, + uint256 enabledTokensMaskAfter, + uint256[] memory forbiddenBalances, + uint256 forbiddenTokenMask, + function (uint256) view returns (address) getTokenByMaskFn + ) internal view { + uint256 forbiddenTokensOnAccount = enabledTokensMaskAfter & forbiddenTokenMask; + if (forbiddenTokensOnAccount == 0) return; + + uint256 forbiddenTokensOnAccountBefore = enabledTokensMaskBefore & forbiddenTokenMask; + if (forbiddenTokensOnAccount & ~forbiddenTokensOnAccountBefore != 0) revert ForbiddenTokensException(); + + unchecked { + uint256 i; + for (uint256 tokenMask = 1; tokenMask < forbiddenTokensOnAccountBefore; tokenMask <<= 1) { + if (forbiddenTokensOnAccountBefore & tokenMask != 0) { + if (forbiddenTokensOnAccount & tokenMask != 0) { + address token = getTokenByMaskFn(tokenMask); + uint256 balance = IERC20Helper.balanceOf(token, creditAccount); + if (balance > forbiddenBalances[i]) { + revert ForbiddenTokensException(); + } + } + + ++i; + } + } + } + } +} diff --git a/contracts/libraries/CollateralLogic.sol b/contracts/libraries/CollateralLogic.sol new file mode 100644 index 00000000..bbf9a06a --- /dev/null +++ b/contracts/libraries/CollateralLogic.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: MIT +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +import {IERC20Helper} from "./IERC20Helper.sol"; +import {CollateralDebtData} from "../interfaces/ICreditManagerV3.sol"; +import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; + +import {BitMask} from "./BitMask.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {RAY} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; + +import "forge-std/console.sol"; + +/// @title Collateral logic Library +library CollateralLogic { + using BitMask for uint256; + + function calcCollateral( + CollateralDebtData memory collateralDebtData, + address creditAccount, + address underlying, + bool lazy, + uint16 minHealthFactor, + uint256[] memory collateralHints, + function (uint256, bool) view returns (address, uint16) collateralTokensByMaskFn, + function (address, uint256, address) view returns(uint256) convertToUSDFn, + address priceOracle + ) internal view returns (uint256 totalValueUSD, uint256 twvUSD, uint256 tokensToDisable) { + uint256 limit = lazy ? collateralDebtData.totalDebtUSD * minHealthFactor / PERCENTAGE_FACTOR : type(uint256).max; + + if (collateralDebtData.quotedTokens.length != 0) { + uint256 underlyingPriceRAY = convertToUSDFn(priceOracle, RAY, underlying); + + (totalValueUSD, twvUSD) = calcQuotedTokensCollateral({ + collateralDebtData: collateralDebtData, + creditAccount: creditAccount, + underlyingPriceRAY: underlyingPriceRAY, + limit: limit, + convertToUSDFn: convertToUSDFn, + priceOracle: priceOracle + }); // U:[CLL-5] + + if (twvUSD >= limit) { + return (totalValueUSD, twvUSD, 0); // U:[CLL-5] + } else { + unchecked { + limit -= twvUSD; // U:[CLL-5] + } + } + } + + // @notice Computes non-quotes collateral + + { + uint256 tokensToCheckMask = + collateralDebtData.enabledTokensMask.disable(collateralDebtData.quotedTokensMask); // U:[CLL-5] + + uint256 tvDelta; + uint256 twvDelta; + + (tvDelta, twvDelta, tokensToDisable) = calcNonQuotedTokensCollateral({ + tokensToCheckMask: tokensToCheckMask, + priceOracle: priceOracle, + creditAccount: creditAccount, + limit: limit, + collateralHints: collateralHints, + collateralTokensByMaskFn: collateralTokensByMaskFn, + convertToUSDFn: convertToUSDFn + }); // U:[CLL-5] + + totalValueUSD += tvDelta; // U:[CLL-5] + twvUSD += twvDelta; // U:[CLL-5] + } + } + + function calcQuotedTokensCollateral( + CollateralDebtData memory collateralDebtData, + address creditAccount, + uint256 underlyingPriceRAY, + uint256 limit, + function (address, uint256, address) view returns(uint256) convertToUSDFn, + address priceOracle + ) internal view returns (uint256 totalValueUSD, uint256 twvUSD) { + uint256 len = collateralDebtData.quotedTokens.length; // U:[CLL-4] + + for (uint256 i; i < len;) { + address token = collateralDebtData.quotedTokens[i]; // U:[CLL-4] + if (token == address(0)) break; // U:[CLL-4] + { + uint16 liquidationThreshold = collateralDebtData.quotedLts[i]; // U:[CLL-4] + uint256 quotaUSD = collateralDebtData.quotas[i] * underlyingPriceRAY / RAY; // U:[CLL-4] + + (uint256 valueUSD, uint256 weightedValueUSD,) = calcOneTokenCollateral({ + priceOracle: priceOracle, + creditAccount: creditAccount, + token: token, + liquidationThreshold: liquidationThreshold, + quotaUSD: quotaUSD, + convertToUSDFn: convertToUSDFn + }); // U:[CLL-4] + + totalValueUSD += valueUSD; // U:[CLL-4] + twvUSD += weightedValueUSD; // U:[CLL-4] + } + if (twvUSD >= limit) { + return (totalValueUSD, twvUSD); // U:[CLL-4] + } + + unchecked { + ++i; + } + } + } + + function calcNonQuotedTokensCollateral( + address creditAccount, + uint256 limit, + uint256[] memory collateralHints, + function (address, uint256, address) view returns(uint256) convertToUSDFn, + function (uint256, bool) view returns (address, uint16) collateralTokensByMaskFn, + uint256 tokensToCheckMask, + address priceOracle + ) internal view returns (uint256 totalValueUSD, uint256 twvUSD, uint256 tokensToDisable) { + uint256 len = collateralHints.length; // U:[CLL-3] + + address ca = creditAccount; // U:[CLL-3] + // TODO: add test that we check all values and it's always reachable + for (uint256 i; tokensToCheckMask != 0;) { + uint256 tokenMask; + unchecked { + // TODO: add check for super long collateralnhints and for double masks + tokenMask = (i < len) ? collateralHints[i] : 1 << (i - len); // U:[CLL-3] + } + + if (tokensToCheckMask & tokenMask != 0) { + bool nonZero; + { + uint256 valueUSD; + uint256 weightedValueUSD; + (valueUSD, weightedValueUSD, nonZero) = calcOneNonQuotedCollateral({ + priceOracle: priceOracle, + creditAccount: ca, + tokenMask: tokenMask, + convertToUSDFn: convertToUSDFn, + collateralTokensByMaskFn: collateralTokensByMaskFn + }); // U:[CLL-3] + totalValueUSD += valueUSD; // U:[CLL-3] + twvUSD += weightedValueUSD; // U:[CLL-3] + } + if (nonZero) { + // Full collateral check evaluates a Credit Account's health factor lazily; + // Once the TWV computed thus far exceeds the debt, the check is considered + // successful, and the function returns without evaluating any further collateral + if (twvUSD >= limit) { + break; // U:[CLL-3] + } + // Zero-balance tokens are disabled; this is done by flipping the + // bit in enabledTokensMask, which is then written into storage at the + // very end, to avoid redundant storage writes + } else { + tokensToDisable |= tokenMask; // U:[CLL-3] + } + } + tokensToCheckMask = tokensToCheckMask.disable(tokenMask); // U:[CLL-3] + + unchecked { + ++i; + } + } + } + + function calcOneNonQuotedCollateral( + address creditAccount, + function (address, uint256, address) view returns(uint256) convertToUSDFn, + function (uint256, bool) view returns (address, uint16) collateralTokensByMaskFn, + uint256 tokenMask, + address priceOracle + ) internal view returns (uint256 valueUSD, uint256 weightedValueUSD, bool nonZeroBalance) { + (address token, uint16 liquidationThreshold) = collateralTokensByMaskFn(tokenMask, true); // U:[CLL-2] + + (valueUSD, weightedValueUSD, nonZeroBalance) = calcOneTokenCollateral({ + priceOracle: priceOracle, + creditAccount: creditAccount, + token: token, + liquidationThreshold: liquidationThreshold, + quotaUSD: type(uint256).max, + convertToUSDFn: convertToUSDFn + }); // U:[CLL-2] + } + + function calcOneTokenCollateral( + address creditAccount, + function (address, uint256, address) view returns(uint256) convertToUSDFn, + address priceOracle, + address token, + uint16 liquidationThreshold, + uint256 quotaUSD + ) internal view returns (uint256 valueUSD, uint256 weightedValueUSD, bool nonZeroBalance) { + uint256 balance = IERC20Helper.balanceOf(token, creditAccount); // U:[CLL-1] + + // Collateral calculations are only done if there is a non-zero balance + if (balance > 1) { + unchecked { + valueUSD = convertToUSDFn(priceOracle, balance - 1, token); // U:[CLL-1] + } + weightedValueUSD = Math.min(valueUSD, quotaUSD) * liquidationThreshold / PERCENTAGE_FACTOR; // U:[CLL-1] + nonZeroBalance = true; // U:[CLL-1] + } + } +} diff --git a/contracts/libraries/CreditAccountHelper.sol b/contracts/libraries/CreditAccountHelper.sol index ce34cd3f..ec47184c 100644 --- a/contracts/libraries/CreditAccountHelper.sol +++ b/contracts/libraries/CreditAccountHelper.sol @@ -8,6 +8,10 @@ import {IERC20Helper} from "./IERC20Helper.sol"; import {ICreditAccount} from "../interfaces/ICreditAccount.sol"; import {AllowanceFailedException} from "../interfaces/IExceptions.sol"; + +import {IWETHGateway} from "../interfaces/IWETHGateway.sol"; +import {IWithdrawalManager} from "../interfaces/IWithdrawalManager.sol"; + /// @title CreditAccount Helper library library CreditAccountHelper { @@ -15,8 +19,9 @@ library CreditAccountHelper { function safeApprove(ICreditAccount creditAccount, address token, address spender, uint256 amount) internal { if (!_approve(creditAccount, token, spender, amount, false)) { - _approve(creditAccount, token, spender, 0, true); // F: - _approve(creditAccount, token, spender, amount, true); + // U:[CAH-1,2] + _approve(creditAccount, token, spender, 0, true); //U:[CAH-1,2] + _approve(creditAccount, token, spender, amount, true); // U:[CAH-1,2] } } @@ -45,18 +50,16 @@ library CreditAccountHelper { return false; } - function _safeTransfer(ICreditAccount creditAccount, address token, address to, uint256 amount) internal { + function transfer(ICreditAccount creditAccount, address token, address to, uint256 amount) internal { ICreditAccount(creditAccount).safeTransfer(token, to, amount); } - function safeTransferDeliveredBalanceControl( - ICreditAccount creditAccount, - address token, - address to, - uint256 amount - ) internal returns (uint256 delivered) { - uint256 balanceBefore = IERC20(token)._balanceOf(to); - _safeTransfer(creditAccount, token, to, amount); - delivered = IERC20(token)._balanceOf(to) - balanceBefore; + function transferDeliveredBalanceControl(ICreditAccount creditAccount, address token, address to, uint256 amount) + internal + returns (uint256 delivered) + { + uint256 balanceBefore = IERC20Helper.balanceOf(token, to); + transfer(creditAccount, token, to, amount); + delivered = IERC20Helper.balanceOf(token, to) - balanceBefore; } } diff --git a/contracts/libraries/CreditLogic.sol b/contracts/libraries/CreditLogic.sol index 70f930de..5ca28831 100644 --- a/contracts/libraries/CreditLogic.sol +++ b/contracts/libraries/CreditLogic.sol @@ -3,19 +3,35 @@ // (c) Gearbox Holdings, 2022 pragma solidity ^0.8.17; -import {BitMask} from "./BitMask.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {CollateralDebtData, CollateralTokenData} from "../interfaces/ICreditManagerV3.sol"; +import {IERC20Helper} from "./IERC20Helper.sol"; import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; -import {Balance} from "@gearbox-protocol/core-v2/contracts/libraries/Balances.sol"; +import {SECONDS_PER_YEAR} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; import "../interfaces/IExceptions.sol"; +import {BitMask} from "./BitMask.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {RAY} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; +import {Balance} from "@gearbox-protocol/core-v2/contracts/libraries/Balances.sol"; + uint256 constant INDEX_PRECISION = 10 ** 9; +import "forge-std/console.sol"; + /// @title Credit Logic Library library CreditLogic { using BitMask for uint256; + function calcLinearGrowth(uint256 value, uint256 timestampLastUpdate) internal view returns (uint256) { + // timeDifference = blockTime - previous timeStamp + + // timeDifference + // valueGrowth = value * ------------------- + // SECONDS_PER_YEAR + // + return value * (block.timestamp - timestampLastUpdate) / SECONDS_PER_YEAR; + } + function calcAccruedInterest(uint256 amount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow) internal pure @@ -79,6 +95,7 @@ library CreditLogic { if (totalFunds > amountToPoolWithFee) { remainingFunds = totalFunds - amountToPoolWithFee - 1; // F:[CM-43] } else { + amountToPoolWithFee = totalFunds; amountToPool = amountMinusFeeFn(totalFunds); // F:[CM-43] } @@ -89,15 +106,7 @@ library CreditLogic { } } - amountToPool = amountWithFeeFn(amountToPool); - } - - function _calcAmountToPool(uint256 debt, uint256 debtWithInterest, uint16 feeInterest) - internal - pure - returns (uint256 amountToPool) - { - amountToPool = debtWithInterest + ((debtWithInterest - debt) * feeInterest) / PERCENTAGE_FACTOR; + amountToPool = amountToPoolWithFee; } function getTokenOrRevert(CollateralTokenData storage tokenData) internal view returns (address token) { @@ -137,19 +146,7 @@ library CreditLogic { /// MANAGE DEBT - /// @dev Calculates the new cumulative index when debt is updated - /// @param debt Current debt principal - /// @param delta Absolute value of total debt amount change - /// @param cumulativeIndexNow Current cumulative index of the pool - /// @param cumulativeIndexOpen Last updated cumulative index recorded for the corresponding debt position - /// @notice Handles two potential cases: - /// * Debt principal is increased by delta - in this case, the principal is changed - /// but the interest / fees have to stay the same - /// * Interest is decreased by delta - in this case, the principal stays the same, - /// but the interest changes. The delta is assumed to have fee repayment excluded. - /// The debt decrease case where delta > interest + fees is trivial and should be handled outside - /// this function. - function calcIncrease(uint256 debt, uint256 delta, uint256 cumulativeIndexNow, uint256 cumulativeIndexOpen) + function calcIncrease(uint256 amount, uint256 debt, uint256 cumulativeIndexNow, uint256 cumulativeIndexLastUpdate) internal pure returns (uint256 newDebt, uint256 newCumulativeIndex) @@ -159,21 +156,21 @@ library CreditLogic { // debt * (cumulativeIndexNow / cumulativeIndexOpen - 1) == // == (debt + delta) * (cumulativeIndexNow / newCumulativeIndex - 1) - newDebt = debt + delta; + newDebt = debt + amount; newCumulativeIndex = ( (cumulativeIndexNow * newDebt * INDEX_PRECISION) - / ((INDEX_PRECISION * cumulativeIndexNow * debt) / cumulativeIndexOpen + INDEX_PRECISION * delta) + / ((INDEX_PRECISION * cumulativeIndexNow * debt) / cumulativeIndexLastUpdate + INDEX_PRECISION * amount) ); } - function calcDescrease( + function calcDecrease( uint256 amount, - uint256 quotaInterestAccrued, - uint16 feeInterest, uint256 debt, uint256 cumulativeIndexNow, - uint256 cumulativeIndexLastUpdate + uint256 cumulativeIndexLastUpdate, + uint256 cumulativeQuotaInterest, + uint16 feeInterest ) internal pure @@ -182,25 +179,25 @@ library CreditLogic { uint256 newCumulativeIndex, uint256 amountToRepay, uint256 profit, - uint256 cumulativeQuotaInterest + uint256 newCumulativeQuotaInterest ) { amountToRepay = amount; - if (quotaInterestAccrued > 1) { - uint256 quotaProfit = (quotaInterestAccrued * feeInterest) / PERCENTAGE_FACTOR; + if (cumulativeQuotaInterest != 0) { + uint256 quotaProfit = (cumulativeQuotaInterest * feeInterest) / PERCENTAGE_FACTOR; - if (amountToRepay >= quotaInterestAccrued + quotaProfit) { - amountToRepay -= quotaInterestAccrued + quotaProfit; // F: [CMQ-5] + if (amountToRepay >= cumulativeQuotaInterest + quotaProfit) { + amountToRepay -= cumulativeQuotaInterest + quotaProfit; // F: [CMQ-5] profit += quotaProfit; // F: [CMQ-5] - cumulativeQuotaInterest = 1; // F: [CMQ-5] + newCumulativeQuotaInterest = 0; // F: [CMQ-5] } else { uint256 amountToPool = (amountToRepay * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR + feeInterest); profit += amountToRepay - amountToPool; // F: [CMQ-4] amountToRepay = 0; // F: [CMQ-4] - cumulativeQuotaInterest = quotaInterestAccrued - amountToPool + 1; // F: [CMQ-4] + newCumulativeQuotaInterest = cumulativeQuotaInterest - amountToPool; // F: [CMQ-4] newDebt = debt; newCumulativeIndex = cumulativeIndexLastUpdate; @@ -209,7 +206,7 @@ library CreditLogic { if (amountToRepay > 0) { // Computes the interest accrued thus far - uint256 interestAccrued = (debt * newCumulativeIndex) / cumulativeIndexLastUpdate - debt; // F:[CM-21] + uint256 interestAccrued = (debt * cumulativeIndexNow) / cumulativeIndexLastUpdate - debt; // F:[CM-21] // Computes profit, taken as a percentage of the interest rate uint256 profitFromInterest = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; // F:[CM-21] @@ -257,103 +254,9 @@ library CreditLogic { - (INDEX_PRECISION * amountToPool * cumulativeIndexLastUpdate) / debt ); } - } - - // TODO: delete after tests or write Invaraiant test - require(debt - newDebt == amountToRepay, "Ooops, something was wring"); - } - - /// @param creditAccount Credit Account to compute balances for - /// @param callData Bytes calldata for parsing - function storeBalances(address creditAccount, bytes memory callData) - internal - view - returns (Balance[] memory expected) - { - // Retrieves the balance list from calldata - expected = abi.decode(callData, (Balance[])); // F:[FA-45] - uint256 len = expected.length; // F:[FA-45] - - for (uint256 i = 0; i < len;) { - expected[i].balance += _balanceOf(expected[i].token, creditAccount); // F:[FA-45] - unchecked { - ++i; - } - } - } - - /// @dev Compares current balances to previously saved expected balances. - /// Reverts if at least one balance is lower than expected - /// @param creditAccount Credit Account to check - /// @param expected Expected balances after all operations - - function compareBalances(address creditAccount, Balance[] memory expected) internal view { - uint256 len = expected.length; // F:[FA-45] - unchecked { - for (uint256 i = 0; i < len; ++i) { - if (_balanceOf(expected[i].token, creditAccount) < expected[i].balance) { - revert BalanceLessThanMinimumDesiredException(expected[i].token); - } // F:[FA-45] - } - } - } - - function _balanceOf(address token, address holder) internal view returns (uint256) { - return IERC20(token).balanceOf(holder); - } - - function storeForbiddenBalances( - address creditAccount, - uint256 enabledTokensMask, - uint256 forbiddenTokenMask, - function (uint256) view returns (address) getTokenByMaskFn - ) internal view returns (uint256[] memory forbiddenBalances) { - uint256 forbiddenTokensOnAccount = enabledTokensMask & forbiddenTokenMask; - - if (forbiddenTokensOnAccount != 0) { - forbiddenBalances = new uint256[](forbiddenTokensOnAccount.calcEnabledTokens()); - unchecked { - uint256 i; - for (uint256 tokenMask = 1; tokenMask < forbiddenTokensOnAccount; tokenMask <<= 1) { - if (forbiddenTokensOnAccount & tokenMask != 0) { - address token = getTokenByMaskFn(tokenMask); - forbiddenBalances[i] = _balanceOf(token, creditAccount); - ++i; - } - } - } - } - } - - function checkForbiddenBalances( - address creditAccount, - uint256 enabledTokensMaskBefore, - uint256 enabledTokensMaskAfter, - uint256[] memory forbiddenBalances, - uint256 forbiddenTokenMask, - function (uint256) view returns (address) getTokenByMaskFn - ) internal view { - uint256 forbiddenTokensOnAccount = enabledTokensMaskAfter & forbiddenTokenMask; - if (forbiddenTokensOnAccount == 0) return; - - uint256 forbiddenTokensOnAccountBefore = enabledTokensMaskBefore & forbiddenTokenMask; - if (forbiddenTokensOnAccount & ~forbiddenTokensOnAccountBefore != 0) revert ForbiddenTokensException(); - - unchecked { - uint256 i; - for (uint256 tokenMask = 1; tokenMask < forbiddenTokensOnAccountBefore; tokenMask <<= 1) { - if (forbiddenTokensOnAccountBefore & tokenMask != 0) { - if (forbiddenTokensOnAccount & tokenMask != 0) { - address token = getTokenByMaskFn(tokenMask); - uint256 balance = _balanceOf(token, creditAccount); - if (balance > forbiddenBalances[i]) { - revert ForbiddenTokensException(); - } - } - - ++i; - } - } + } else { + newDebt = debt; + newCumulativeIndex = cumulativeIndexLastUpdate; } } } diff --git a/contracts/libraries/IERC20Helper.sol b/contracts/libraries/IERC20Helper.sol index 02564222..86513317 100644 --- a/contracts/libraries/IERC20Helper.sol +++ b/contracts/libraries/IERC20Helper.sol @@ -8,8 +8,8 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title IERC20HelperTrait /// @notice Saves size by providing internal call for balanceOf library IERC20Helper { - function _balanceOf(IERC20 token, address holder) internal view returns (uint256) { - return token.balanceOf(holder); + function balanceOf(address token, address holder) internal view returns (uint256) { + return IERC20(token).balanceOf(holder); } function unsafeTransfer(IERC20 token, address to, uint256 amount) internal returns (bool success) { diff --git a/contracts/libraries/QuotasLogic.sol b/contracts/libraries/QuotasLogic.sol index 5938864e..486b1620 100644 --- a/contracts/libraries/QuotasLogic.sol +++ b/contracts/libraries/QuotasLogic.sol @@ -145,6 +145,9 @@ library QuotasLogic { { uint96 quoted = accountQuota.quota; + // Unlike general quota updates, quota removals do not update accountQuota.cumulativeIndexLU to save gas + // This is safe, since the quota is set to 1 and the index will be updated to the correct value on next change from + // zero to non-zero, without breaking any interest calculations if (quoted > 1) { quoted--; diff --git a/contracts/libraries/USDTFees.sol b/contracts/libraries/USDTFees.sol new file mode 100644 index 00000000..ecd589bd --- /dev/null +++ b/contracts/libraries/USDTFees.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; + +/// @title USDT fees Library +library USDTFees { + function amountUSDTWithFee(uint256 amount, uint256 basisPointsRate, uint256 maximumFee) + internal + pure + returns (uint256) + { + uint256 amountWithBP = (amount * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR - basisPointsRate); // U:[UTT_01] + unchecked { + uint256 amountWithMaxFee = maximumFee > type(uint256).max - amount ? type(uint256).max : amount + maximumFee; + return Math.min(amountWithMaxFee, amountWithBP); // U:[UTT_01] + } + } + + /// @dev Computes how much usdt you should send to get exact amount on destination account + function amountUSDTMinusFee(uint256 amount, uint256 basisPointsRate, uint256 maximumFee) + internal + pure + returns (uint256) + { + uint256 fee = amount * basisPointsRate / PERCENTAGE_FACTOR; + fee = Math.min(maximumFee, fee); + return amount - fee; + } +} diff --git a/contracts/libraries/WithdrawalsLogic.sol b/contracts/libraries/WithdrawalsLogic.sol index 152acef1..19c522fc 100644 --- a/contracts/libraries/WithdrawalsLogic.sol +++ b/contracts/libraries/WithdrawalsLogic.sol @@ -5,12 +5,12 @@ pragma solidity ^0.8.17; import {ClaimAction, ScheduledWithdrawal} from "../interfaces/IWithdrawalManager.sol"; -/// @title Withdrawals library +/// @title Withdrawals logic library library WithdrawalsLogic { /// @dev Clears withdrawal in storage function clear(ScheduledWithdrawal storage w) internal { - w.maturity = 1; // F: [WL-1] - w.amount = 1; // F: [WL-1] + w.maturity = 1; // U:[WL-1] + w.amount = 1; // U:[WL-1] } /// @dev If withdrawal is scheduled, returns withdrawn token, its mask in credit manager and withdrawn amount @@ -22,9 +22,9 @@ library WithdrawalsLogic { uint256 amount_ = w.amount; if (amount_ > 1) { unchecked { - token = w.token; // F: [WL-2] - mask = 1 << w.tokenIndex; // F: [WL-2] - amount = amount_ - 1; // F: [WL-2] + token = w.token; // U:[WL-2] + mask = 1 << w.tokenIndex; // U:[WL-2] + amount = amount_ - 1; // U:[WL-2] } } } @@ -32,26 +32,26 @@ library WithdrawalsLogic { /// @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 < 2) { - found = true; // F: [WL-3] + found = true; // U:[WL-3] } else if (ws[1].maturity < 2) { - found = true; // F: [WL-3] - slot = 1; // F: [WL-3] + 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 < 2) return false; // F: [WL-4] - if (action == ClaimAction.FORCE_CANCEL) return false; // F: [WL-4] - if (action == ClaimAction.FORCE_CLAIM) return true; // F: [WL-4] - return block.timestamp >= maturity; // F: [WL-4] + if (maturity < 2) 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 < 2) return false; // F: [WL-5] - if (action == ClaimAction.FORCE_CANCEL) return true; // F: [WL-5] - if (action == ClaimAction.FORCE_CLAIM || action == ClaimAction.CLAIM) return false; // F: [WL-5] - return block.timestamp < maturity; // F: [WL-5] + if (maturity < 2) 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/Gauge.sol b/contracts/pool/Gauge.sol index 90d0b0a8..7244db7a 100644 --- a/contracts/pool/Gauge.sol +++ b/contracts/pool/Gauge.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; pragma abicoder v1; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -21,7 +21,7 @@ import {IGearStaking} from "../interfaces/IGearStaking.sol"; import {RAY, SECONDS_PER_YEAR, MAX_WITHDRAW_FEE} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; -import {Pool4626} from "./Pool4626.sol"; +import {PoolV3} from "./PoolV3.sol"; // EXCEPTIONS import "../interfaces/IExceptions.sol"; @@ -35,7 +35,7 @@ contract Gauge is IGauge, ACLNonReentrantTrait { address public immutable addressProvider; /// @dev Address of the pool - Pool4626 public immutable pool; + PoolV3 public immutable pool; /// @dev Mapping from token address to its rate parameters mapping(address => QuotaRateParams) public quotaRateParams; @@ -59,14 +59,14 @@ contract Gauge is IGauge, ACLNonReentrantTrait { /// @dev Constructor constructor(address _pool, address _gearStaking) - ACLNonReentrantTrait(address(Pool4626(_pool).addressProvider())) + ACLNonReentrantTrait(address(PoolV3(_pool).addressProvider())) nonZeroAddress(_pool) nonZeroAddress(_gearStaking) { // Additional check that receiver is not address(0) - addressProvider = address(Pool4626(_pool).addressProvider()); // F:[P4-01] - pool = Pool4626(_pool); // F:[P4-01] + addressProvider = address(PoolV3(_pool).addressProvider()); // F:[P4-01] + pool = PoolV3(_pool); // F:[P4-01] voter = IGearStaking(_gearStaking); epochLU = voter.getCurrentEpoch(); } diff --git a/contracts/pool/LinearInterestRateModel.sol b/contracts/pool/LinearInterestRateModel.sol index fb57dc8e..2c0ed350 100644 --- a/contracts/pool/LinearInterestRateModel.sol +++ b/contracts/pool/LinearInterestRateModel.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; pragma abicoder v1; import {WAD, RAY} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; @@ -9,7 +9,7 @@ import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/P import {IInterestRateModel} from "../interfaces/IInterestRateModel.sol"; // EXCEPTIONS -import "../interfaces/IExceptions.sol"; +import {IncorrectParameterException, BorrowingMoreU2ForbiddenException} from "../interfaces/IExceptions.sol"; /// @title Linear Interest Rate Model contract LinearInterestRateModel is IInterestRateModel { @@ -40,7 +40,7 @@ contract LinearInterestRateModel is IInterestRateModel { /// @dev Constructor /// @param U_1 Optimal U in percentage format: x10.000 - percentage plus two decimals /// @param U_2 Optimal U in percentage format: x10.000 - percentage plus two decimals - /// @param R_base R_base in percentage format: x10.000 - percentage plus two decimals @param R_slope1 R_Slope1 in Ray + /// @param R_base R_base in percentage format: x10.000 - percentage plus two decimals /// @param R_slope1 R_Slope1 in percentage format: x10.000 - percentage plus two decimals /// @param R_slope2 R_Slope2 in percentage format: x10.000 - percentage plus two decimals /// @param R_slope3 R_Slope3 in percentage format: x10.000 - percentage plus two decimals @@ -58,21 +58,21 @@ contract LinearInterestRateModel is IInterestRateModel { || (R_slope1 > PERCENTAGE_FACTOR) || (R_slope2 > PERCENTAGE_FACTOR) || (R_slope1 > R_slope2) || (R_slope2 > R_slope3) ) { - revert IncorrectParameterException(); // F:[LIM-2] + revert IncorrectParameterException(); // U:[LIM-2] } // Convert percetns to WAD - U_1_WAD = (WAD * U_1) / PERCENTAGE_FACTOR; // F:[LIM-1] + U_1_WAD = (WAD * U_1) / PERCENTAGE_FACTOR; // U:[LIM-1] // Convert percetns to WAD - U_2_WAD = (WAD * U_2) / PERCENTAGE_FACTOR; // F:[LIM-1] + U_2_WAD = (WAD * U_2) / PERCENTAGE_FACTOR; // U:[LIM-1] - R_base_RAY = (RAY * R_base) / PERCENTAGE_FACTOR; // F:[LIM-1] - R_slope1_RAY = (RAY * R_slope1) / PERCENTAGE_FACTOR; // F:[LIM-1] - R_slope2_RAY = (RAY * R_slope2) / PERCENTAGE_FACTOR; // F:[LIM-1] - R_slope3_RAY = (RAY * R_slope3) / PERCENTAGE_FACTOR; // F:[LIM-1] + R_base_RAY = (RAY * R_base) / PERCENTAGE_FACTOR; // U:[LIM-1] + R_slope1_RAY = (RAY * R_slope1) / PERCENTAGE_FACTOR; // U:[LIM-1] + R_slope2_RAY = (RAY * R_slope2) / PERCENTAGE_FACTOR; // U:[LIM-1] + R_slope3_RAY = (RAY * R_slope3) / PERCENTAGE_FACTOR; // U:[LIM-1] - isBorrowingMoreU2Forbidden = _isBorrowingMoreU2Forbidden; // F:[LIM-1] + isBorrowingMoreU2Forbidden = _isBorrowingMoreU2Forbidden; // U:[LIM-1] } /// @dev Returns the borrow rate calculated based on expectedLiquidity and availableLiquidity @@ -100,13 +100,13 @@ contract LinearInterestRateModel is IInterestRateModel { { if (expectedLiquidity == 0 || expectedLiquidity < availableLiquidity) { return R_base_RAY; - } // F:[LIM-3] + } // U:[LIM-3] // expectedLiquidity - availableLiquidity // U = ------------------------------------- // expectedLiquidity - uint256 U_WAD = (WAD * (expectedLiquidity - availableLiquidity)) / expectedLiquidity; // F:[LIM-3] + uint256 U_WAD = (WAD * (expectedLiquidity - availableLiquidity)) / expectedLiquidity; // U:[LIM-3] // if U < U1: // @@ -115,7 +115,7 @@ contract LinearInterestRateModel is IInterestRateModel { // U1 // if (U_WAD < U_1_WAD) { - return R_base_RAY + ((R_slope1_RAY * U_WAD) / U_1_WAD); // F:[LIM-3] + return R_base_RAY + ((R_slope1_RAY * U_WAD) / U_1_WAD); // U:[LIM-3] } // if U >= U1 & U < U2: @@ -125,12 +125,12 @@ contract LinearInterestRateModel is IInterestRateModel { // U2 - U1 if (U_WAD >= U_1_WAD && U_WAD < U_2_WAD) { - return R_base_RAY + R_slope1_RAY + (R_slope2_RAY * (U_WAD - U_1_WAD)) / (U_2_WAD - U_1_WAD); // F:[LIM-3] + return R_base_RAY + R_slope1_RAY + (R_slope2_RAY * (U_WAD - U_1_WAD)) / (U_2_WAD - U_1_WAD); // U:[LIM-3] } /// if U > U2 && checkOptimalBorrowing && isBorrowingMoreU2Forbidden if (checkOptimalBorrowing && isBorrowingMoreU2Forbidden) { - revert BorrowingMoreU2ForbiddenException(); // F:[LIM-3] + revert BorrowingMoreU2ForbiddenException(); // U:[LIM-3] } // if U >= U2: @@ -139,7 +139,7 @@ contract LinearInterestRateModel is IInterestRateModel { // borrowRate = Rbase + Rslope1 + Rslope2 + Rslope * ---------- // 1 - U2 - return R_base_RAY + R_slope1_RAY + R_slope2_RAY + (R_slope3_RAY * (U_WAD - U_2_WAD)) / (WAD - U_2_WAD); // F:[LIM-3] + return R_base_RAY + R_slope1_RAY + R_slope2_RAY + (R_slope3_RAY * (U_WAD - U_2_WAD)) / (WAD - U_2_WAD); // U:[LIM-3] } /// @dev Returns the model's parameters @@ -152,12 +152,12 @@ contract LinearInterestRateModel is IInterestRateModel { view returns (uint16 U_1, uint16 U_2, uint16 R_base, uint16 R_slope1, uint16 R_slope2, uint16 R_slope3) { - U_1 = uint16((U_1_WAD * PERCENTAGE_FACTOR) / WAD); // F:[LIM-1] - U_2 = uint16((U_2_WAD * PERCENTAGE_FACTOR) / WAD); // F:[LIM-1] - R_base = uint16(R_base_RAY * PERCENTAGE_FACTOR / RAY); // F:[LIM-1] - R_slope1 = uint16(R_slope1_RAY * PERCENTAGE_FACTOR / RAY); // F:[LIM-1] - R_slope2 = uint16(R_slope2_RAY * PERCENTAGE_FACTOR / RAY); // F:[LIM-1] - R_slope3 = uint16(R_slope3_RAY * PERCENTAGE_FACTOR / RAY); // F:[LIM-1] + U_1 = uint16((U_1_WAD * PERCENTAGE_FACTOR) / WAD); // U:[LIM-1] + U_2 = uint16((U_2_WAD * PERCENTAGE_FACTOR) / WAD); // U:[LIM-1] + R_base = uint16(R_base_RAY * PERCENTAGE_FACTOR / RAY); // U:[LIM-1] + R_slope1 = uint16(R_slope1_RAY * PERCENTAGE_FACTOR / RAY); // U:[LIM-1] + R_slope2 = uint16(R_slope2_RAY * PERCENTAGE_FACTOR / RAY); // U:[LIM-1] + R_slope3 = uint16(R_slope3_RAY * PERCENTAGE_FACTOR / RAY); // U:[LIM-1] } function availableToBorrow(uint256 expectedLiquidity, uint256 availableLiquidity) @@ -167,11 +167,11 @@ contract LinearInterestRateModel is IInterestRateModel { returns (uint256) { if (isBorrowingMoreU2Forbidden && (expectedLiquidity >= availableLiquidity)) { - uint256 U_WAD = (WAD * (expectedLiquidity - availableLiquidity)) / expectedLiquidity; // F:[LIM-3] + uint256 U_WAD = (WAD * (expectedLiquidity - availableLiquidity)) / expectedLiquidity; // U:[LIM-3] - return (U_WAD < U_2_WAD) ? ((U_2_WAD - U_WAD) * expectedLiquidity) / WAD : 0; // F:[LIM-3] + return (U_WAD < U_2_WAD) ? ((U_2_WAD - U_WAD) * expectedLiquidity) / WAD : 0; // U:[LIM-3] } else { - return availableLiquidity; // F:[LIM-3] + return availableLiquidity; // U:[LIM-3] } } } diff --git a/contracts/pool/PoolQuotaKeeper.sol b/contracts/pool/PoolQuotaKeeper.sol index c0e3c139..f8e7139e 100644 --- a/contracts/pool/PoolQuotaKeeper.sol +++ b/contracts/pool/PoolQuotaKeeper.sol @@ -10,7 +10,6 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {AddressProvider} from "@gearbox-protocol/core-v2/contracts/core/AddressProvider.sol"; -import {IPriceOracleV2} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; /// LIBS & TRAITS import {ACLNonReentrantTrait} from "../traits/ACLNonReentrantTrait.sol"; @@ -19,7 +18,7 @@ import {CreditLogic} from "../libraries/CreditLogic.sol"; import {QuotasLogic} from "../libraries/QuotasLogic.sol"; -import {IPool4626} from "../interfaces/IPool4626.sol"; +import {IPoolV3} from "../interfaces/IPoolV3.sol"; import {IPoolQuotaKeeper, TokenQuotaParams, AccountQuota} from "../interfaces/IPoolQuotaKeeper.sol"; import {IGauge} from "../interfaces/IGauge.sol"; import {ICreditManagerV3} from "../interfaces/ICreditManagerV3.sol"; @@ -43,7 +42,7 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait, ContractsReg address public immutable underlying; /// @dev Address of the protocol treasury - IPool4626 public immutable override pool; + address public immutable override pool; /// @dev The list of all Credit Managers EnumerableSet.AddressSet internal creditManagerSet; @@ -87,11 +86,11 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait, ContractsReg /// @dev Constructor /// @param _pool Pool address constructor(address _pool) - ACLNonReentrantTrait(address(IPool4626(_pool).addressProvider())) - ContractsRegisterTrait(address(IPool4626(_pool).addressProvider())) + ACLNonReentrantTrait(IPoolV3(_pool).addressProvider()) + ContractsRegisterTrait(IPoolV3(_pool).addressProvider()) { - pool = IPool4626(_pool); // F:[PQK-1] - underlying = IPool4626(_pool).asset(); // F:[PQK-1] + pool = _pool; // F:[PQK-1] + underlying = IPoolV3(_pool).asset(); // F:[PQK-1] } /// @dev Updates credit account's accountQuotas for multiple tokens @@ -102,9 +101,10 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait, ContractsReg creditManagerOnly // F:[PQK-4] returns (uint256 caQuotaInterestChange, bool enableToken, bool disableToken) { - TokenQuotaParams storage tokenQuotaParams = totalQuotaParams[token]; + int128 quotaRevenueChange; // TODO: better naming(?) + AccountQuota storage accountQuota = accountQuotas[creditAccount][token]; - int128 quotaRevenueChange; + TokenQuotaParams storage tokenQuotaParams = totalQuotaParams[token]; (caQuotaInterestChange, quotaRevenueChange, enableToken, disableToken) = QuotasLogic.changeQuota({ tokenQuotaParams: tokenQuotaParams, @@ -114,7 +114,7 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait, ContractsReg }); if (quotaRevenueChange != 0) { - pool.changeQuotaRevenue(quotaRevenueChange); + IPoolV3(pool).changeQuotaRevenue(quotaRevenueChange); } } @@ -149,8 +149,8 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait, ContractsReg } } - if (quotaRevenueChange > 0) { - pool.changeQuotaRevenue(quotaRevenueChange); + if (quotaRevenueChange != 0) { + IPoolV3(pool).changeQuotaRevenue(quotaRevenueChange); } } @@ -161,22 +161,19 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait, ContractsReg external override creditManagerOnly // F:[PQK-4] - returns (uint256 caQuotaInterestChange) { uint256 len = tokens.length; - - for (uint256 i; i < len;) { - address token = tokens[i]; - if (token == address(0)) break; - - caQuotaInterestChange += QuotasLogic.accrueAccountQuotaInterest({ - tokenQuotaParams: totalQuotaParams[token], - accountQuota: accountQuotas[creditAccount][token], - lastQuotaRateUpdate: lastQuotaRateUpdate - }); - - unchecked { - ++i; + uint40 _lastQuotaRateUpdate = lastQuotaRateUpdate; + unchecked { + for (uint256 i; i < len; ++i) { + address token = tokens[i]; + if (token == address(0)) break; + + QuotasLogic.accrueAccountQuotaInterest({ + tokenQuotaParams: totalQuotaParams[token], + accountQuota: accountQuotas[creditAccount][token], + lastQuotaRateUpdate: _lastQuotaRateUpdate + }); } } } @@ -184,75 +181,17 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait, ContractsReg // // GETTERS // - - /// @dev Computes outstanding quota interest - function outstandingQuotaInterest(address creditAccount, address[] memory tokens) + function getQuotaAndOutstandingInterest(address creditAccount, address token) external view override - returns (uint256 caQuotaInterestChange) - { - uint256 len = tokens.length; - for (uint256 i; i < len;) { - address token = tokens[i]; - if (token == address(0)) break; - - caQuotaInterestChange += QuotasLogic.calcOutstandingQuotaInterest({ - tokenQuotaParams: totalQuotaParams[token], - accountQuota: accountQuotas[creditAccount][token], - lastQuotaRateUpdate: lastQuotaRateUpdate - }); - - unchecked { - ++i; - } - } - } - - /// @dev Computes collateral value for quoted tokens on the account, as well as accrued quota interest - function computeQuotedCollateralUSD( - address creditAccount, - address _priceOracle, - address[] memory tokens, - uint256[] memory lts - ) external view override returns (uint256 totalValueUSD, uint256 twvUSD, uint256 totalQuotaInterest) { - uint256 len = tokens.length; - for (uint256 i; i < len;) { - address token = tokens[i]; - if (token == address(0)) break; - - (uint256 currentUSD, uint256 outstandingInterest) = _getCollateralValue(creditAccount, token, _priceOracle); // F:[CMQ-8] - - totalValueUSD += currentUSD; - twvUSD += currentUSD * lts[i]; // F:[CMQ-8] - totalQuotaInterest += outstandingInterest; // F:[CMQ-8] - - unchecked { - ++i; - } - } - - twvUSD /= PERCENTAGE_FACTOR; - } - - /// @dev Gets the effective value (i.e., value in underlying included into TWV) for a quoted token on an account - function _getCollateralValue(address creditAccount, address token, address _priceOracle) - internal - view - returns (uint256 value, uint256 interest) + returns (uint256 quoted, uint256 interest) { AccountQuota storage accountQuota = accountQuotas[creditAccount][token]; - uint96 quoted = accountQuota.quota; + quoted = accountQuota.quota; if (quoted > 1) { - uint256 quotaValueUSD = IPriceOracleV2(_priceOracle).convertToUSD(quoted, underlying); // F:[CMQ-8] - uint256 balance = IERC20(token).balanceOf(creditAccount); - if (balance > 1) { - value = IPriceOracleV2(_priceOracle).convertToUSD(balance, token); // F:[CMQ-8] - if (value > quotaValueUSD) value = quotaValueUSD; // F:[CMQ-8] - } - interest = CreditLogic.calcAccruedInterest({ amount: quoted, cumulativeIndexLastUpdate: accountQuota.cumulativeIndexLU, @@ -344,7 +283,7 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait, ContractsReg } } - pool.updateQuotaRevenue(quotaRevenue); // F:[PQK-7] + IPoolV3(pool).updateQuotaRevenue(quotaRevenue); // F:[PQK-7] lastQuotaRateUpdate = uint40(block.timestamp); // F:[PQK-7] } diff --git a/contracts/pool/Pool4626.sol b/contracts/pool/PoolV3.sol similarity index 93% rename from contracts/pool/Pool4626.sol rename to contracts/pool/PoolV3.sol index 9bce8b6d..3fa6ef88 100644 --- a/contracts/pool/Pool4626.sol +++ b/contracts/pool/PoolV3.sol @@ -16,22 +16,24 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import {AddressProvider} from "@gearbox-protocol/core-v2/contracts/core/AddressProvider.sol"; +import {IAddressProviderV3, AP_TREASURY, NO_VERSION_CONTROL} from "../interfaces/IAddressProviderV3.sol"; /// LIBS & TRAITS import {ACLNonReentrantTrait} from "../traits/ACLNonReentrantTrait.sol"; import {ContractsRegisterTrait} from "../traits/ContractsRegisterTrait.sol"; +import {CreditLogic} from "../libraries/CreditLogic.sol"; import {IInterestRateModel} from "../interfaces/IInterestRateModel.sol"; -import {IPool4626} from "../interfaces/IPool4626.sol"; +import {IPoolV3} from "../interfaces/IPoolV3.sol"; import {ICreditManagerV3} from "../interfaces/ICreditManagerV3.sol"; import {IPoolQuotaKeeper} from "../interfaces/IPoolQuotaKeeper.sol"; -import {RAY, SECONDS_PER_YEAR, MAX_WITHDRAW_FEE} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; +import {RAY, MAX_WITHDRAW_FEE, SECONDS_PER_YEAR} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; // EXCEPTIONS import "../interfaces/IExceptions.sol"; +import "forge-std/console.sol"; struct CreditManagerDebt { uint128 totalBorrowed; @@ -40,13 +42,14 @@ struct CreditManagerDebt { /// @title Core pool contract compatible with ERC4626 /// @notice Implements pool & diesel token business logic -contract Pool4626 is ERC4626, IPool4626, ACLNonReentrantTrait, ContractsRegisterTrait { +contract PoolV3 is ERC4626, IPoolV3, ACLNonReentrantTrait, ContractsRegisterTrait { using Math for uint256; using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; + using CreditLogic for uint256; /// @dev Address provider - AddressProvider public immutable override addressProvider; + address public immutable override addressProvider; /// @dev Address of the protocol treasury address public immutable treasury; @@ -82,7 +85,7 @@ contract Pool4626 is ERC4626, IPool4626, ACLNonReentrantTrait, ContractsRegister uint64 public override timestampLU; /// @dev Interest rate model - IInterestRateModel public interestRateModel; + address public interestRateModel; /// @dev Withdrawal fee in PERCENTAGE FORMAT uint16 public override withdrawFee; @@ -150,15 +153,16 @@ contract Pool4626 is ERC4626, IPool4626, ACLNonReentrantTrait, ContractsRegister nonZeroAddress(_underlyingToken) // F:[P4-02] nonZeroAddress(_interestRateModel) // F:[P4-02] { - addressProvider = AddressProvider(_addressProvider); // F:[P4-01] + addressProvider = _addressProvider; // F:[P4-01] underlyingToken = _underlyingToken; // F:[P4-01] - treasury = AddressProvider(_addressProvider).getTreasuryContract(); // F:[P4-01] + treasury = + IAddressProviderV3(_addressProvider).getAddressOrRevert({key: AP_TREASURY, _version: NO_VERSION_CONTROL}); // F:[P4-01] timestampLU = uint64(block.timestamp); // F:[P4-01] cumulativeIndexLU_RAY = uint128(RAY); // F:[P4-01] - interestRateModel = IInterestRateModel(_interestRateModel); + interestRateModel = _interestRateModel; emit SetInterestRateModel(_interestRateModel); // F:[P4-03] _setExpectedLiquidityLimit(_expectedLiquidityLimit); // F:[P4-01, 03] @@ -415,21 +419,12 @@ contract Pool4626 is ERC4626, IPool4626, ACLNonReentrantTrait, ContractsRegister /// @dev Computes interest rate accrued from last update (LU) function _calcBaseInterestAccrued() internal view returns (uint256) { - // timeDifference = blockTime - previous timeStamp - - // currentBorrowRate * timeDifference - // interestAccrued = totalBorrow * ------------------------------------ - // SECONDS_PER_YEAR - // - - // TODO: move to lib - return (uint256(_totalBorrowed) * _borrowRate * (block.timestamp - timestampLU)) / RAY / SECONDS_PER_YEAR; + // TODO: add comment why we divide by RAY + return (uint256(_totalBorrowed) * _borrowRate).calcLinearGrowth(timestampLU) / RAY; } function _calcOutstandingQuotaRevenue() internal view returns (uint128) { - return uint128( - (quotaRevenue * (block.timestamp - lastQuotaRevenueUpdate)) / (SECONDS_PER_YEAR * PERCENTAGE_FACTOR) - ); // F:[P4-17] + return uint128(uint256(quotaRevenue).calcLinearGrowth(lastQuotaRevenueUpdate) / PERCENTAGE_FACTOR); // F:[P4-17] } /// @dev Returns available liquidity in the pool (pool balance) @@ -536,10 +531,7 @@ contract Pool4626 is ERC4626, IPool4626, ACLNonReentrantTrait, ContractsRegister /// /// @return Current cumulative index in RAY function calcLinearCumulative_RAY() public view override returns (uint256) { - uint256 timeDifference = block.timestamp - timestampLU; // F:[P4-15] - uint256 linearAccumulated_RAY = RAY + (_borrowRate * timeDifference) / SECONDS_PER_YEAR; // F:[P4-15] - - return (cumulativeIndexLU_RAY * linearAccumulated_RAY) / RAY; // F:[P4-15] + return cumulativeIndexLU_RAY * (RAY + uint256(_borrowRate).calcLinearGrowth(timestampLU)) / RAY; // F:[P4-15] } /// @dev Updates core popo when liquidity parameters are changed @@ -560,7 +552,7 @@ contract Pool4626 is ERC4626, IPool4626, ACLNonReentrantTrait, ContractsRegister // update borrow APY // TODO: add case to check with quotas _borrowRate = uint128( - interestRateModel.calcBorrowRate( + IInterestRateModel(interestRateModel).calcBorrowRate( updatedExpectedLiquidityLU + (supportsQuotas ? _calcOutstandingQuotaRevenue() : 0), availableLiquidityChanged == 0 ? availableLiquidity() @@ -643,7 +635,7 @@ contract Pool4626 is ERC4626, IPool4626, ACLNonReentrantTrait, ContractsRegister configuratorOnly // F:[P4-18] nonZeroAddress(_interestRateModel) { - interestRateModel = IInterestRateModel(_interestRateModel); // F:[P4-22] + interestRateModel = _interestRateModel; // F:[P4-22] _updateBaseParameters(0, 0, false); // F:[P4-22] @@ -652,7 +644,7 @@ contract Pool4626 is ERC4626, IPool4626, ACLNonReentrantTrait, ContractsRegister /// @dev Sets the new pool quota keeper /// @param _poolQuotaKeeper Address of the new poolQuotaKeeper copntract - function connectPoolQuotaManager(address _poolQuotaKeeper) + function setPoolQuotaManager(address _poolQuotaKeeper) external override configuratorOnly // F:[P4-18] @@ -738,9 +730,10 @@ contract Pool4626 is ERC4626, IPool4626, ACLNonReentrantTrait, ContractsRegister _totalBorrowedLimit == type(uint128).max ? type(uint256).max : _totalBorrowedLimit - _totalBorrowed; } // F:[P4-27] - uint256 available = interestRateModel.availableToBorrow(expectedLiquidity(), availableLiquidity()); // F:[P4-27] + uint256 available = + IInterestRateModel(interestRateModel).availableToBorrow(expectedLiquidity(), availableLiquidity()); // F:[P4-27] - canBorrow = Math.max(canBorrow, available); // F:[P4-27] + canBorrow = Math.min(canBorrow, available); // F:[P4-27] CreditManagerDebt memory cmDebt = creditManagersDebt[_creditManager]; if (cmDebt.totalBorrowed >= cmDebt.limit) { @@ -749,7 +742,7 @@ contract Pool4626 is ERC4626, IPool4626, ACLNonReentrantTrait, ContractsRegister unchecked { uint256 cmLimit = cmDebt.limit - cmDebt.totalBorrowed; - canBorrow = Math.max(canBorrow, cmLimit); // F:[P4-27] + canBorrow = Math.min(canBorrow, cmLimit); // F:[P4-27] } } diff --git a/contracts/pool/Pool4626_USDT.sol b/contracts/pool/PoolV3_USDT.sol similarity index 75% rename from contracts/pool/Pool4626_USDT.sol rename to contracts/pool/PoolV3_USDT.sol index 9bd55fab..2705facb 100644 --- a/contracts/pool/Pool4626_USDT.sol +++ b/contracts/pool/PoolV3_USDT.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; -import {Pool4626} from "./Pool4626.sol"; +import {PoolV3} from "./PoolV3.sol"; import {USDT_Transfer} from "../traits/USDT_Transfer.sol"; -import {IPool4626} from "../interfaces/IPool4626.sol"; +import {IPoolV3} from "../interfaces/IPoolV3.sol"; /// @title Core pool contract compatible with ERC4626 /// @notice Implements pool & dieselUSDT_Transferogic -contract Pool4626_USDT is Pool4626, USDT_Transfer { +contract PoolV3_USDT is PoolV3, USDT_Transfer { constructor( address _addressProvider, address _underlyingToken, @@ -18,7 +18,7 @@ contract Pool4626_USDT is Pool4626, USDT_Transfer { uint256 _expectedLiquidityLimit, bool _supportsQuotas ) - Pool4626(_addressProvider, _underlyingToken, _interestRateModel, _expectedLiquidityLimit, _supportsQuotas) + PoolV3(_addressProvider, _underlyingToken, _interestRateModel, _expectedLiquidityLimit, _supportsQuotas) USDT_Transfer(_underlyingToken) { // Additional check that receiver is not address(0) diff --git a/contracts/support/BotList.sol b/contracts/support/BotList.sol index 63135031..3104a23c 100644 --- a/contracts/support/BotList.sol +++ b/contracts/support/BotList.sol @@ -1,14 +1,18 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox. Generalized leverage protocol that allows to take leverage and then use it across other DeFi protocols and platforms in a composable way. // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +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 {ACLNonReentrantTrait} from "../traits/ACLNonReentrantTrait.sol"; import {IBotList, BotFunding} from "../interfaces/IBotList.sol"; import {IAddressProvider} from "@gearbox-protocol/core-v2/contracts/interfaces/IAddressProvider.sol"; +import {ICreditManagerV3} from "../interfaces/ICreditManagerV3.sol"; +import {ICreditFacade} from "../interfaces/ICreditFacade.sol"; +import {ICreditAccount} from "../interfaces/ICreditAccount.sol"; import "../interfaces/IExceptions.sol"; import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; @@ -20,14 +24,25 @@ contract BotList is ACLNonReentrantTrait, IBotList { using SafeCast for uint256; using Address for address; using Address for address payable; + using EnumerableSet for EnumerableSet.AddressSet; - /// @dev Mapping from (borrower, bot) to bot approval status + /// @dev Mapping from Credit Manager address to their status as an approved Credit Manager + /// Only Credit Facades connected to approved Credit Managers can alter bot permissions + mapping(address => bool) public approvedCreditManager; + + /// @dev Mapping from (creditAccount, bot) to bit permissions mapping(address => mapping(address => uint192)) public botPermissions; + /// @dev Mapping from credit account to the set of bots with non-zero permissions + mapping(address => EnumerableSet.AddressSet) internal activeBots; + /// @dev Whether the bot is forbidden system-wide mapping(address => bool) public forbiddenBot; - /// @dev Mapping of (borrower, bot) to bot funding parameters + /// @dev Mapping from borrower to their bot funding balance + mapping(address => uint256) public fundingBalances; + + /// @dev Mapping of (creditAccount, bot) to bot funding parameters mapping(address => mapping(address => BotFunding)) public botFunding; /// @dev A fee (in PERCENTAGE_FACTOR format) charged by the DAO on bot payments @@ -43,98 +58,162 @@ contract BotList is ACLNonReentrantTrait, IBotList { treasury = IAddressProvider(_addressProvider).getTreasuryContract(); } - /// @dev Adds or removes allowance for a bot to execute multicalls on behalf of sender - /// @param bot Bot address - /// @param permissions Whether allowance is added or removed - function setBotPermissions(address bot, uint192 permissions) external nonZeroAddress(bot) { + /// @dev Limits access to a function only to Credit Facades connected to approved CMs + modifier onlyValidCreditFacade() { + address creditManager = ICreditFacade(msg.sender).creditManager(); + if (!approvedCreditManager[creditManager] || ICreditManagerV3(creditManager).creditFacade() != msg.sender) { + revert CallerNotCreditFacadeException(); + } + _; + } + + /// @dev Sets permissions and funding for (creditAccount, bot). Callable only through CreditFacade + /// @param creditAccount CA to set permissions for + /// @param bot Bot to set permissions for + /// @param permissions A bit mask of permissions + /// @param fundingAmount Total amount of ETH available to the bot for payments + /// @param weeklyFundingAllowance Amount of ETH available to the bot weekly + function setBotPermissions( + address creditAccount, + address bot, + uint192 permissions, + uint72 fundingAmount, + uint72 weeklyFundingAllowance + ) + external + nonZeroAddress(bot) + onlyValidCreditFacade // F: [BL-03] + returns (uint256 activeBotsRemaining) + { if (!bot.isContract()) { - revert AddressIsNotContractException(bot); + revert AddressIsNotContractException(bot); // F: [BL-03] } if (forbiddenBot[bot] && permissions != 0) { - revert InvalidBotException(); + revert InvalidBotException(); // F: [BL-03] } - botPermissions[msg.sender][bot] = permissions; - - emit ApproveBot(msg.sender, bot, permissions); - } + if (permissions != 0) { + activeBots[creditAccount].add(bot); // F: [BL-03] + } else if (permissions == 0) { + if (fundingAmount != 0 || weeklyFundingAllowance != 0) { + revert PositiveFundingForInactiveBotException(); // F: [BL-03] + } - /// @dev Adds funds to user's balance for a particular bot. The entire sent value in ETH is added - /// @param bot Address of the bot to fund - function increaseBotFunding(address bot) external payable nonReentrant { - if (msg.value == 0) { - revert AmountCantBeZeroException(); + activeBots[creditAccount].remove(bot); // F: [BL-03] } - if (forbiddenBot[bot] || botPermissions[msg.sender][bot] == 0) { - revert InvalidBotException(); - } + botPermissions[creditAccount][bot] = permissions; // F: [BL-03] + botFunding[creditAccount][bot].remainingFunds = fundingAmount; // F: [BL-03] + botFunding[creditAccount][bot].maxWeeklyAllowance = weeklyFundingAllowance; // F: [BL-03] + botFunding[creditAccount][bot].remainingWeeklyAllowance = weeklyFundingAllowance; // F: [BL-03] + botFunding[creditAccount][bot].allowanceLU = uint40(block.timestamp); // F: [BL-03] - uint72 newRemainingFunds = botFunding[msg.sender][bot].remainingFunds + msg.value.toUint72(); + activeBotsRemaining = activeBots[creditAccount].length(); // F: [BL-03] + + emit SetBotPermissions(creditAccount, bot, permissions, fundingAmount, weeklyFundingAllowance); // F: [BL-03] + } - botFunding[msg.sender][bot].remainingFunds = newRemainingFunds; + /// @dev Removes permissions and funding for all bots with non-zero permissions for a credit account + /// @param creditAccount Credit Account to erase permissions for + function eraseAllBotPermissions(address creditAccount) + external + onlyValidCreditFacade // F: [BL-06] + { + uint256 len = activeBots[creditAccount].length(); + + for (uint256 i = 0; i < len;) { + address bot = activeBots[creditAccount].at(0); // F: [BL-06] + botPermissions[creditAccount][bot] = 0; // F: [BL-06] + botFunding[creditAccount][bot].remainingFunds = 0; // F: [BL-06] + botFunding[creditAccount][bot].maxWeeklyAllowance = 0; // F: [BL-06] + botFunding[creditAccount][bot].remainingWeeklyAllowance = 0; // F: [BL-06] + botFunding[creditAccount][bot].allowanceLU = uint40(block.timestamp); // F: [BL-06] + activeBots[creditAccount].remove(bot); // F: [BL-06] + unchecked { + ++i; + } + } - emit ChangeBotFunding(msg.sender, bot, newRemainingFunds); + if (len > 0) { + emit EraseBots(creditAccount); // F: [BL-06] + } } - /// @dev Removes funds from the user's balance for a particular bot. The funds are sent to the user. - /// @param bot Address of the bot to remove funds from - /// @param decreaseAmount Amount to remove - function decreaseBotFunding(address bot, uint72 decreaseAmount) external nonReentrant { - if (decreaseAmount == 0) { - revert AmountCantBeZeroException(); + /// @dev Takes payment for performed services from the user's balance and sends to the bot + /// @param payer Address to charge + /// @param creditAccount Address of the credit account paid for + /// @param bot Address of the bot to pay + /// @param paymentAmount Amount to pay + function payBot(address payer, address creditAccount, address bot, uint72 paymentAmount) + external + onlyValidCreditFacade + { + if (paymentAmount == 0) { + revert AmountCantBeZeroException(); // F: [BL-05] } - uint72 newRemainingFunds = botFunding[msg.sender][bot].remainingFunds - decreaseAmount; + BotFunding storage bf = botFunding[creditAccount][bot]; // F: [BL-05] - botFunding[msg.sender][bot].remainingFunds = newRemainingFunds; - payable(msg.sender).sendValue(decreaseAmount); + if (block.timestamp >= bf.allowanceLU + uint40(7 days)) { + bf.allowanceLU = uint40(block.timestamp); // F: [BL-05] + bf.remainingWeeklyAllowance = bf.maxWeeklyAllowance; // F: [BL-05] + } - emit ChangeBotFunding(msg.sender, bot, newRemainingFunds); - } + uint72 feeAmount = daoFee * paymentAmount / PERCENTAGE_FACTOR; // F: [BL-05] - /// @dev Sets the amount that can be pull by the bot per week - /// @param bot Address of the bot to set allowance for - /// @param allowanceAmount Amount of weekly allowance - function setWeeklyBotAllowance(address bot, uint72 allowanceAmount) external nonReentrant { - BotFunding memory bf = botFunding[msg.sender][bot]; + bf.remainingWeeklyAllowance -= paymentAmount + feeAmount; // F: [BL-05] + bf.remainingFunds -= paymentAmount + feeAmount; // F: [BL-05] - bf.maxWeeklyAllowance = allowanceAmount; - bf.remainingWeeklyAllowance = - bf.remainingWeeklyAllowance > allowanceAmount ? allowanceAmount : bf.remainingWeeklyAllowance; + fundingBalances[payer] -= uint256(paymentAmount + feeAmount); // F: [BL-05] - botFunding[msg.sender][bot] = bf; + payable(bot).sendValue(paymentAmount); // F: [BL-05] + if (feeAmount > 0) payable(treasury).sendValue(feeAmount); // F: [BL-05] - emit ChangeBotWeeklyAllowance(msg.sender, bot, allowanceAmount); + emit PayBot(payer, creditAccount, bot, paymentAmount, feeAmount); // F: [BL-05] } - /// @dev Takes payment from the user to the bot for performed services - /// @param payer Address of the paying user - /// @param paymentAmount Amount to pull - function pullPayment(address payer, uint72 paymentAmount) external nonReentrant { - if (paymentAmount == 0) { - revert AmountCantBeZeroException(); + /// @dev Adds funds to the borrower's bot payment wallet + function addFunding() external payable nonReentrant { + if (msg.value == 0) { + revert AmountCantBeZeroException(); // F: [BL-04] } - BotFunding memory bf = botFunding[payer][msg.sender]; + uint256 newFunds = fundingBalances[msg.sender] + msg.value; // F: [BL-04] - if (block.timestamp >= bf.allowanceLU + uint40(7 days)) { - bf.allowanceLU = uint40(block.timestamp); - bf.remainingWeeklyAllowance = bf.maxWeeklyAllowance; - } + fundingBalances[msg.sender] = newFunds; // F: [BL-04] + + emit ChangeFunding(msg.sender, newFunds); // F: [BL-04] + } - uint72 feeAmount = daoFee * paymentAmount / PERCENTAGE_FACTOR; + /// @dev Removes funds from the borrower's bot payment wallet + function removeFunding(uint256 amount) external nonReentrant { + uint256 newFunds = fundingBalances[msg.sender] - amount; // F: [BL-04] - bf.remainingWeeklyAllowance -= paymentAmount + feeAmount; - bf.remainingFunds -= paymentAmount + feeAmount; + fundingBalances[msg.sender] = newFunds; // F: [BL-04] + payable(msg.sender).sendValue(amount); // F: [BL-04] - botFunding[payer][msg.sender] = bf; + emit ChangeFunding(msg.sender, newFunds); // F: [BL-04] + } + + /// @dev Returns all active bots currently on the account + function getActiveBots(address creditAccount) external view returns (address[] memory) { + return activeBots[creditAccount].values(); + } - payable(msg.sender).sendValue(paymentAmount); - if (feeAmount > 0) payable(treasury).sendValue(feeAmount); + /// @dev Returns information about bot permissions + function getBotStatus(address bot, address creditAccount) + external + view + returns (uint192 permissions, bool forbidden) + { + return (botPermissions[creditAccount][bot], forbiddenBot[bot]); + } - emit PullBotPayment(payer, msg.sender, paymentAmount, feeAmount); + /// @dev Internal function to retrieve the bot's owner + function _getCreditAccountOwner(address creditAccount) internal view returns (address owner) { + address creditManager = ICreditAccount(creditAccount).creditManager(); + return ICreditManagerV3(creditManager).getBorrowerOrRevert(creditAccount); } // @@ -150,8 +229,21 @@ contract BotList is ACLNonReentrantTrait, IBotList { /// @dev Sets the DAO fee on bot payments /// @param newFee The new fee value function setDAOFee(uint16 newFee) external configuratorOnly { - daoFee = newFee; + daoFee = newFee; // F: [BL-02] - emit SetBotDAOFee(newFee); + emit SetBotDAOFee(newFee); // F: [BL-02] + } + + /// @dev Sets an address' status as an approved Credit Manager + /// @param creditManager Address of the Credit Manager to change status for + /// @param status The new status + function setApprovedCreditManagerStatus(address creditManager, bool status) external configuratorOnly { + approvedCreditManager[creditManager] = status; + + if (status) { + emit CreditManagerAdded(creditManager); + } else { + emit CreditManagerRemoved(creditManager); + } } } diff --git a/contracts/support/DataCompressor.sol b/contracts/support/DataCompressor.sol index 617d4b53..5ab779fc 100644 --- a/contracts/support/DataCompressor.sol +++ b/contracts/support/DataCompressor.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; pragma experimental ABIEncoderV2; +import "../interfaces/IAddressProviderV3.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; @@ -15,7 +16,7 @@ import {ICreditFilter} from "@gearbox-protocol/core-v2/contracts/interfaces/V1/I import {ICreditConfigurator} from "../interfaces/ICreditConfiguratorV3.sol"; import {ICreditAccount} from "@gearbox-protocol/core-v2/contracts/interfaces/ICreditAccount.sol"; import {IPoolService} from "@gearbox-protocol/core-v2/contracts/interfaces/IPoolService.sol"; -import {IPool4626} from "../interfaces/IPool4626.sol"; +import {IPoolV3} from "../interfaces/IPoolV3.sol"; import {IVersion} from "@gearbox-protocol/core-v2/contracts/interfaces/IVersion.sol"; @@ -39,7 +40,7 @@ import {ZeroAddressException} from "../interfaces/IExceptions.sol"; /// Do not use for data from any onchain activities contract DataCompressor is IDataCompressor, ContractsRegisterTrait { /// @dev Address of the AddressProvider - AddressProvider public immutable addressProvider; + IAddressProviderV3 public immutable addressProvider; /// @dev Address of the ContractsRegister ContractsRegister public immutable contractsRegister; @@ -53,9 +54,9 @@ contract DataCompressor is IDataCompressor, ContractsRegisterTrait { constructor(address _addressProvider) ContractsRegisterTrait(_addressProvider) { if (_addressProvider == address(0)) revert ZeroAddressException(); - addressProvider = AddressProvider(_addressProvider); - contractsRegister = ContractsRegister(addressProvider.getContractsRegister()); - WETHToken = addressProvider.getWethToken(); + addressProvider = IAddressProviderV3(_addressProvider); + contractsRegister = ContractsRegister(addressProvider.getAddressOrRevert(AP_CONTRACTS_REGISTER, 1)); + WETHToken = addressProvider.getAddressOrRevert(AP_WETH_TOKEN, 0); } /// @dev Returns CreditAccountData for all opened accounts for particular borrower @@ -153,14 +154,14 @@ contract DataCompressor is IDataCompressor, ContractsRegisterTrait { result.borrowedAmount = ICreditAccount(creditAccount).borrowedAmount(); - result.borrowedAmountPlusInterest = creditFilter.calcCreditAccountAccruedInterest(creditAccount); + // result.borrowedAmountPlusInterest = creditFilter.calcAccruedInterestAndFees(creditAccount); } else { result.underlying = creditManagerV2.underlying(); // (result.totalValue,) = creditFacade.calcTotalValue(creditAccount); // result.healthFactor = creditFacade.calcCreditAccountHealthFactor(creditAccount); // (result.borrowedAmount, result.borrowedAmountPlusInterest, result.borrowedAmountPlusInterestAndFees) = - // creditManagerV2.calcCreditAccountAccruedInterest(creditAccount); + // creditManagerV2.calcAccruedInterestAndFees(creditAccount); } address pool = address((ver == 1) ? creditManager.poolService() : creditManagerV2.pool()); @@ -181,7 +182,7 @@ contract DataCompressor is IDataCompressor, ContractsRegisterTrait { (balance.token, balance.balance,,) = creditFilter.getCreditAccountTokenById(creditAccount, i); balance.isAllowed = creditFilter.isTokenAllowed(balance.token); } else { - (balance.token,) = creditManagerV2.collateralTokens(i); + (balance.token,) = creditManagerV2.collateralTokensByMask(1 << i); balance.balance = IERC20(balance.token).balanceOf(creditAccount); // TODO: change balance.isAllowed = true; //creditFacade.isAllowToken(balance.token); @@ -260,7 +261,7 @@ contract DataCompressor is IDataCompressor, ContractsRegisterTrait { result.liquidationThresholds[i] = creditFilter.liquidationThresholds(token); } else { (result.collateralTokens[i], result.liquidationThresholds[i]) = - creditManagerV2.collateralTokens(i); + creditManagerV2.collateralTokensByMask(1 << i); } } } @@ -311,7 +312,7 @@ contract DataCompressor is IDataCompressor, ContractsRegisterTrait { result.degenNFT = creditFacade.degenNFT(); result.isIncreaseDebtForbidden = creditFacade.maxDebtPerBlockMultiplier() == 0; // V2 only: true if increasing debt is forbidden result.forbiddenTokenMask = creditFacade.forbiddenTokenMask(); // V2 only: mask which forbids some particular tokens - result.maxEnabledTokensLength = creditManagerV2.maxAllowedEnabledTokenLength(); // V2 only: a limit on enabled tokens imposed for security + result.maxEnabledTokensLength = creditManagerV2.maxEnabledTokens(); // V2 only: a limit on enabled tokens imposed for security { ( result.feeInterest, @@ -349,7 +350,7 @@ contract DataCompressor is IDataCompressor, ContractsRegisterTrait { uint256 dieselSupply = IERC20(result.dieselToken).totalSupply(); uint256 totalLP = - (result.version > 1) ? IPool4626(_pool).convertToAssets(dieselSupply) : pool.fromDiesel(dieselSupply); + (result.version > 1) ? IPoolV3(_pool).convertToAssets(dieselSupply) : pool.fromDiesel(dieselSupply); result.depositAPY_RAY = totalLP == 0 ? result.borrowAPY_RAY diff --git a/contracts/support/WETHGateway.sol b/contracts/support/WETHGateway.sol index f44980a6..67ae8fdb 100644 --- a/contracts/support/WETHGateway.sol +++ b/contracts/support/WETHGateway.sol @@ -1,20 +1,20 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; pragma abicoder v1; +import "../interfaces/IAddressProviderV3.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {AddressProvider} from "@gearbox-protocol/core-v2/contracts/core/AddressProvider.sol"; import {ContractsRegisterTrait} from "../traits/ContractsRegisterTrait.sol"; import {IPoolService} from "@gearbox-protocol/core-v2/contracts/interfaces/IPoolService.sol"; -import {IPool4626} from "../interfaces/IPool4626.sol"; +import {IPoolV3} from "../interfaces/IPoolV3.sol"; import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; import {IWETHGateway} from "../interfaces/IWETHGateway.sol"; @@ -68,7 +68,7 @@ contract WETHGateway is IWETHGateway, ReentrancyGuard, ContractsRegisterTrait { /// @param addressProvider Address Repository for upgradable contract model constructor(address addressProvider) ContractsRegisterTrait(addressProvider) { if (addressProvider == address(0)) revert ZeroAddressException(); - weth = AddressProvider(addressProvider).getWethToken(); + weth = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_WETH_TOKEN, 0); } /// FOR POOLS V3 @@ -83,7 +83,7 @@ contract WETHGateway is IWETHGateway, ReentrancyGuard, ContractsRegisterTrait { IWETH(weth).deposit{value: msg.value}(); _checkAllowance(pool, msg.value); - return IPool4626(pool).deposit(msg.value, receiver); + return IPoolV3(pool).deposit(msg.value, receiver); } function depositReferral(address pool, address receiver, uint16 referralCode) @@ -96,7 +96,7 @@ contract WETHGateway is IWETHGateway, ReentrancyGuard, ContractsRegisterTrait { IWETH(weth).deposit{value: msg.value}(); _checkAllowance(pool, msg.value); - return IPool4626(pool).depositReferral(msg.value, receiver, referralCode); + return IPoolV3(pool).depositReferral(msg.value, receiver, referralCode); } function mint(address pool, uint256 shares, address receiver) @@ -110,7 +110,7 @@ contract WETHGateway is IWETHGateway, ReentrancyGuard, ContractsRegisterTrait { IWETH(weth).deposit{value: msg.value}(); _checkAllowance(pool, msg.value); - assets = IPool4626(pool).mint(shares, receiver); + assets = IPoolV3(pool).mint(shares, receiver); } function withdraw(address pool, uint256 assets, address receiver, address owner) @@ -120,7 +120,7 @@ contract WETHGateway is IWETHGateway, ReentrancyGuard, ContractsRegisterTrait { unwrapAndTransferWethTo(receiver) returns (uint256 shares) { - return IPool4626(pool).withdraw(assets, address(this), owner); + return IPoolV3(pool).withdraw(assets, address(this), owner); } function redeem(address pool, uint256 shares, address receiver, address owner) @@ -130,7 +130,7 @@ contract WETHGateway is IWETHGateway, ReentrancyGuard, ContractsRegisterTrait { unwrapAndTransferWethTo(receiver) returns (uint256 assets) { - return IPool4626(pool).redeem(shares, address(this), owner); + return IPoolV3(pool).redeem(shares, address(this), owner); } // CREDIT MANAGERS diff --git a/contracts/support/WithdrawalManager.sol b/contracts/support/WithdrawalManager.sol index ce27510a..d985d8a9 100644 --- a/contracts/support/WithdrawalManager.sol +++ b/contracts/support/WithdrawalManager.sol @@ -16,6 +16,7 @@ import {ClaimAction, IWithdrawalManager, IVersion, ScheduledWithdrawal} from ".. import {IERC20Helper} from "../libraries/IERC20Helper.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. @@ -23,7 +24,7 @@ import {ACLTrait} from "../traits/ACLTrait.sol"; /// - Immediate withdrawals can be claimed, well, immediately, and exist to support blacklistable tokens. /// - 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 WithdrawalManager is IWithdrawalManager, ACLTrait { +contract WithdrawalManager is IWithdrawalManager, ACLTrait, ContractsRegisterTrait { using SafeERC20 for IERC20; using IERC20Helper for IERC20; using WithdrawalsLogic for ClaimAction; @@ -33,9 +34,6 @@ contract WithdrawalManager is IWithdrawalManager, ACLTrait { /// @inheritdoc IVersion uint256 public constant override version = 3_00; - /// @inheritdoc IWithdrawalManager - mapping(address => bool) public override creditManagerStatus; - /// @inheritdoc IWithdrawalManager mapping(address => mapping(address => uint256)) public override immediateWithdrawals; @@ -45,19 +43,17 @@ contract WithdrawalManager is IWithdrawalManager, ACLTrait { /// @dev Mapping credit account => scheduled withdrawals mapping(address => ScheduledWithdrawal[2]) internal _scheduled; - /// @notice Ensures that caller of the function is one of registered credit managers - modifier creditManagerOnly() { - if (!creditManagerStatus[msg.sender]) { - revert CallerNotCreditManagerException(); - } - _; - } - /// @notice Constructor /// @param _addressProvider Address of the address provider /// @param _delay Delay for scheduled withdrawals - constructor(address _addressProvider, uint40 _delay) ACLTrait(_addressProvider) { - _setWithdrawalDelay(_delay); // F: [WM-1] + constructor(address _addressProvider, uint40 _delay) + ACLTrait(_addressProvider) + ContractsRegisterTrait(_addressProvider) + { + if (_delay != 0) { + delay = _delay; // U:[WM-1] + } + emit SetWithdrawalDelay(_delay); } /// --------------------- /// @@ -65,19 +61,19 @@ contract WithdrawalManager is IWithdrawalManager, ACLTrait { /// --------------------- /// /// @inheritdoc IWithdrawalManager - function addImmediateWithdrawal(address account, address token, uint256 amount) + function addImmediateWithdrawal(address token, address to, uint256 amount) external override - creditManagerOnly // F: [WM-2] + registeredCreditManagerOnly(msg.sender) // U:[WM-2] { - _addImmediateWithdrawal(account, token, amount); + _addImmediateWithdrawal({account: to, token: token, amount: amount}); } /// @inheritdoc IWithdrawalManager function claimImmediateWithdrawal(address token, address to) external override - nonZeroAddress(to) // F: [WM-4A] + nonZeroAddress(to) // U:[WM-4A] { _claimImmediateWithdrawal(msg.sender, token, to); } @@ -85,21 +81,21 @@ contract WithdrawalManager is IWithdrawalManager, ACLTrait { /// @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; // F: [WM-3] - emit AddImmediateWithdrawal(account, token, amount); // F: [WM-3] + 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) internal { uint256 amount = immediateWithdrawals[account][token]; - if (amount < 2) revert NothingToClaimException(); // F: [WM-4B] + if (amount < 2) revert NothingToClaimException(); // U:[WM-4B] unchecked { - --amount; // F: [WM-4C] + --amount; // U:[WM-4C] } - immediateWithdrawals[account][token] = 1; // F: [WM-4C] - IERC20(token).safeTransfer(to, amount); // F: [WM-4C] - emit ClaimImmediateWithdrawal(account, token, to, amount); // F: [WM-4C] + immediateWithdrawals[account][token] = 1; // U:[WM-4C] + IERC20(token).safeTransfer(to, amount); // U:[WM-4C] + emit ClaimImmediateWithdrawal(account, token, to, amount); // U:[WM-4C] } /// --------------------- /// @@ -120,41 +116,41 @@ contract WithdrawalManager is IWithdrawalManager, ACLTrait { function addScheduledWithdrawal(address creditAccount, address token, uint256 amount, uint8 tokenIndex) external override - creditManagerOnly // F: [WM-2] + registeredCreditManagerOnly(msg.sender) // U:[WM-2] { if (amount < 2) { - revert AmountCantBeZeroException(); // F: [WM-5A] + revert AmountCantBeZeroException(); // U:[WM-5A] } ScheduledWithdrawal[2] storage withdrawals = _scheduled[creditAccount]; - (bool found, uint8 slot) = withdrawals.findFreeSlot(); // F: [WM-5B] - if (!found) revert NoFreeWithdrawalSlotsException(); // F: [WM-5B] + (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}); // F: [WM-5B] - emit AddScheduledWithdrawal(creditAccount, token, amount, maturity); // F: [WM-5B] + ScheduledWithdrawal({tokenIndex: tokenIndex, token: token, maturity: maturity, amount: amount}); // U:[WM-5B] + emit AddScheduledWithdrawal(creditAccount, token, amount, maturity); // U:[WM-5B] } /// @inheritdoc IWithdrawalManager function claimScheduledWithdrawals(address creditAccount, address to, ClaimAction action) external override - creditManagerOnly // F: [WM-2] + registeredCreditManagerOnly(msg.sender) // 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); // F: [WM-6B] + _processScheduledWithdrawal(withdrawals[0], action, creditAccount, to); // U:[WM-6B] (bool scheduled1, bool claimed1, uint256 tokensToEnable1) = - _processScheduledWithdrawal(withdrawals[1], action, creditAccount, to); // F: [WM-6B] + _processScheduledWithdrawal(withdrawals[1], action, creditAccount, to); // U:[WM-6B] if (action == ClaimAction.CLAIM && !(claimed0 || claimed1)) { - revert NothingToClaimException(); // F: [WM-6A] + revert NothingToClaimException(); // U:[WM-6A] } - hasScheduled = scheduled0 || scheduled1; // F: [WM-6B] - tokensToEnable = tokensToEnable0 | tokensToEnable1; // F: [WM-6B] + hasScheduled = scheduled0 || scheduled1; // U:[WM-6B] + tokensToEnable = tokensToEnable0 | tokensToEnable1; // U:[WM-6B] } /// @inheritdoc IWithdrawalManager @@ -165,12 +161,12 @@ contract WithdrawalManager is IWithdrawalManager, ACLTrait { returns (address token1, uint256 amount1, address token2, uint256 amount2) { ScheduledWithdrawal[2] storage withdrawals = _scheduled[creditAccount]; - ClaimAction action = isForceCancel ? ClaimAction.FORCE_CANCEL : ClaimAction.CANCEL; // F: [WM-7] + ClaimAction action = isForceCancel ? ClaimAction.FORCE_CANCEL : ClaimAction.CANCEL; // U:[WM-7] if (action.cancelAllowed(withdrawals[0].maturity)) { - (token1,, amount1) = withdrawals[0].tokenMaskAndAmount(); // F: [WM-7] + (token1,, amount1) = withdrawals[0].tokenMaskAndAmount(); // U:[WM-7] } if (action.cancelAllowed(withdrawals[1].maturity)) { - (token2,, amount2) = withdrawals[1].tokenMaskAndAmount(); // F: [WM-7] + (token2,, amount2) = withdrawals[1].tokenMaskAndAmount(); // U:[WM-7] } } @@ -182,26 +178,26 @@ contract WithdrawalManager is IWithdrawalManager, ACLTrait { address to ) internal returns (bool scheduled, bool claimed, uint256 tokensToEnable) { uint40 maturity = w.maturity; - scheduled = maturity > 1; // F: [WM-8] + scheduled = maturity > 1; // U:[WM-8] if (action.claimAllowed(maturity)) { - _claimScheduledWithdrawal(w, creditAccount, to); // F: [WM-8] - scheduled = false; // F: [WM-8] - claimed = true; // F: [WM-8] + _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); // F: [WM-8] - scheduled = false; // F: [WM-8] + 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(); // F: [WM-9A] - w.clear(); // F: [WM-9A] - emit ClaimScheduledWithdrawal(creditAccount, token, to, amount); // F: [WM-9A] + (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); // F: [WM-9A] - if (!success) _addImmediateWithdrawal(to, token, amount); // F: [WM-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 @@ -210,12 +206,12 @@ contract WithdrawalManager is IWithdrawalManager, ACLTrait { internal returns (uint256 tokensToEnable) { - (address token, uint256 tokenMask, uint256 amount) = w.tokenMaskAndAmount(); // F: [WM-10] - w.clear(); // F: [WM-10] - emit CancelScheduledWithdrawal(creditAccount, token, amount); // F: [WM-10] + (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); // F: [WM-10] - tokensToEnable = tokenMask; // F: [WM-10] + IERC20(token).safeTransfer(creditAccount, amount); // U:[WM-10] + tokensToEnable = tokenMask; // U:[WM-10] } /// ------------- /// @@ -226,28 +222,11 @@ contract WithdrawalManager is IWithdrawalManager, ACLTrait { function setWithdrawalDelay(uint40 _delay) external override - configuratorOnly // F: [WM-2] + configuratorOnly // U:[WM-2] { - _setWithdrawalDelay(_delay); - } - - /// @dev Sets new delay for scheduled withdrawals - function _setWithdrawalDelay(uint40 _delay) internal { if (_delay != delay) { - delay = _delay; // F: [WM-11] - emit SetWithdrawalDelay(_delay); // F: [WM-11] - } - } - - /// @inheritdoc IWithdrawalManager - function setCreditManagerStatus(address creditManager, bool status) - external - override - configuratorOnly // F: [WM-2] - { - if (creditManagerStatus[creditManager] != status) { - creditManagerStatus[creditManager] = status; // F: [WM-12] - emit SetCreditManagerStatus(creditManager, status); // F: [WM-12] + delay = _delay; // U:[WM-11] + emit SetWithdrawalDelay(_delay); // U:[WM-11] } } } diff --git a/contracts/support/risk-controller/ControllerTimelock.sol b/contracts/support/risk-controller/ControllerTimelock.sol index cf9115dd..1942cd4b 100644 --- a/contracts/support/risk-controller/ControllerTimelock.sol +++ b/contracts/support/risk-controller/ControllerTimelock.sol @@ -10,7 +10,7 @@ import {IControllerTimelock, QueuedTransactionData} from "../../interfaces/ICont import {ICreditManagerV3} from "../../interfaces/ICreditManagerV3.sol"; import {ICreditConfigurator} from "../../interfaces/ICreditConfiguratorV3.sol"; import {ICreditFacade} from "../../interfaces/ICreditFacade.sol"; -import {IPool4626} from "../../interfaces/IPool4626.sol"; +import {IPoolV3} from "../../interfaces/IPoolV3.sol"; import {ILPPriceFeed} from "../../interfaces/ILPPriceFeed.sol"; /// @dev @@ -59,7 +59,7 @@ contract ControllerTimelock is PolicyManager, IControllerTimelock { { ICreditFacade creditFacade = ICreditFacade(ICreditManagerV3(creditManager).creditFacade()); address creditConfigurator = ICreditManagerV3(creditManager).creditConfigurator(); - IPool4626 pool = IPool4626(ICreditManagerV3(creditManager).pool()); + IPoolV3 pool = IPoolV3(ICreditManagerV3(creditManager).pool()); uint40 oldExpirationDate = creditFacade.expirationDate(); uint256 totalBorrowed = pool.creditManagerBorrowed(address(creditManager)); @@ -159,7 +159,7 @@ contract ControllerTimelock is PolicyManager, IControllerTimelock { external adminOnly // F: [RCT-05] { - IPool4626 pool = IPool4626(ICreditManagerV3(creditManager).pool()); + IPoolV3 pool = IPoolV3(ICreditManagerV3(creditManager).pool()); uint256 debtLimitCurrent = pool.creditManagerLimit(address(creditManager)); @@ -184,6 +184,7 @@ contract ControllerTimelock is PolicyManager, IControllerTimelock { address creditManager, address token, uint16 liquidationThresholdFinal, + uint40 rampStart, uint24 rampDuration ) external @@ -194,15 +195,17 @@ contract ControllerTimelock is PolicyManager, IControllerTimelock { address creditConfigurator = ICreditManagerV3(creditManager).creditConfigurator(); uint256 ltCurrent = ICreditManagerV3(creditManager).liquidationThresholds(token); - if (!_checkPolicy(policyHash, uint256(ltCurrent), uint256(liquidationThresholdFinal)) || rampDuration < 7 days) - { + if ( + !_checkPolicy(policyHash, uint256(ltCurrent), uint256(liquidationThresholdFinal)) || rampDuration < 7 days + || rampStart < block.timestamp + delay + ) { revert ParameterChecksFailedException(); // F: [RCT-06] } _queueTransaction({ target: creditConfigurator, - signature: "rampLiquidationThreshold(address,uint16,uint24)", - data: abi.encode(token, liquidationThresholdFinal, rampDuration) + signature: "rampLiquidationThreshold(address,uint16,uint40,uint24)", + data: abi.encode(token, liquidationThresholdFinal, rampStart, rampDuration) }); // F: [RCT-06] } diff --git a/contracts/tasks.md b/contracts/tasks.md new file mode 100644 index 00000000..385e1169 --- /dev/null +++ b/contracts/tasks.md @@ -0,0 +1,13 @@ +TASKS: + +- LOAD ONLY FOR INCREASE DEBT +- DEBT_ONLY +- DEBT_COLLATERAL_WITHOUT_WITHDRAWALS +- DEBT_COLLATERAL_CANCEL_WITHDRAWALS +- DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS +- FULL COLLATERAL CHECK (LAZY) + +Blocks: + +- setDebtAndCumulativeIndex(creditAccountInfo storage, \_poolCumulativeIndexNow) => [ debt, cumultaiveIndexLU, cumulativeIndexNow] +- diff --git a/contracts/test/config/CreditConfig.sol b/contracts/test/config/CreditConfig.sol index 819de529..f07f4c0e 100644 --- a/contracts/test/config/CreditConfig.sol +++ b/contracts/test/config/CreditConfig.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {TokensTestSuite} from "../suites/TokensTestSuite.sol"; import {Tokens} from "./Tokens.sol"; diff --git a/contracts/test/config/Tokens.sol b/contracts/test/config/Tokens.sol index 65c46413..cd4b150c 100644 --- a/contracts/test/config/Tokens.sol +++ b/contracts/test/config/Tokens.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox. Generalized leverage protocol that allows to take leverage and then use it across other DeFi protocols and platforms in a composable way. // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; /// @dev c-Tokens and LUNA are added for unit test purposes enum Tokens { diff --git a/contracts/test/config/TokensData.sol b/contracts/test/config/TokensData.sol index fbdc9b97..0f95994d 100644 --- a/contracts/test/config/TokensData.sol +++ b/contracts/test/config/TokensData.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox. Generalized leverage protocol that allows to take leverage and then use it across other DeFi protocols and platforms in a composable way. // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {Tokens} from "./Tokens.sol"; import "../lib/constants.sol"; diff --git a/contracts/test/gas/CreditFacade_Gas.t.sol b/contracts/test/gas/credit/CreditFacade.gas.t.sol similarity index 91% rename from contracts/test/gas/CreditFacade_Gas.t.sol rename to contracts/test/gas/credit/CreditFacade.gas.t.sol index 82c0a24a..45340780 100644 --- a/contracts/test/gas/CreditFacade_Gas.t.sol +++ b/contracts/test/gas/credit/CreditFacade.gas.t.sol @@ -1,22 +1,22 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; -import {CreditFacadeV3} from "../../credit/CreditFacadeV3.sol"; -import {CreditManagerV3} from "../../credit/CreditManagerV3.sol"; +import {CreditFacadeV3} from "../../../credit/CreditFacadeV3.sol"; +import {CreditManagerV3} from "../../../credit/CreditManagerV3.sol"; -import {BotList} from "../../support/BotList.sol"; +import {BotList} from "../../../support/BotList.sol"; -import {ICreditFacade, ICreditFacadeMulticall, ICreditFacadeEvents} from "../../interfaces/ICreditFacade.sol"; -import {ICreditManagerV3, ICreditManagerV3Events, ClosureAction} from "../../interfaces/ICreditManagerV3.sol"; +import {ICreditFacade, ICreditFacadeMulticall, ICreditFacadeEvents} from "../../../interfaces/ICreditFacade.sol"; +import {ICreditManagerV3, ICreditManagerV3Events, ClosureAction} from "../../../interfaces/ICreditManagerV3.sol"; import {AccountFactory} from "@gearbox-protocol/core-v2/contracts/core/AccountFactory.sol"; import {IDegenNFT, IDegenNFTExceptions} from "@gearbox-protocol/core-v2/contracts/interfaces/IDegenNFT.sol"; -import {IWithdrawalManager} from "../../interfaces/IWithdrawalManager.sol"; +import {IWithdrawalManager} from "../../../interfaces/IWithdrawalManager.sol"; // DATA import {MultiCall, MultiCallOps} from "@gearbox-protocol/core-v2/contracts/libraries/MultiCall.sol"; @@ -34,23 +34,23 @@ import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/P // TESTS -import "../lib/constants.sol"; -import {BalanceHelper} from "../helpers/BalanceHelper.sol"; -import {CreditFacadeTestHelper} from "../helpers/CreditFacadeTestHelper.sol"; +import "../../lib/constants.sol"; +import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; +import {CreditFacadeTestHelper} from "../../helpers/CreditFacadeTestHelper.sol"; // EXCEPTIONS -import "../../interfaces/IExceptions.sol"; +import "../../../interfaces/IExceptions.sol"; // MOCKS -import {AdapterMock} from "../mocks/adapters/AdapterMock.sol"; +import {AdapterMock} from "../../mocks//adapters/AdapterMock.sol"; import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/adapters/TargetContractMock.sol"; -import {ERC20BlacklistableMock} from "../mocks/token/ERC20Blacklistable.sol"; +import {ERC20BlacklistableMock} from "../../mocks//token/ERC20Blacklistable.sol"; // SUITES -import {TokensTestSuite} from "../suites/TokensTestSuite.sol"; -import {Tokens} from "../config/Tokens.sol"; -import {CreditFacadeTestSuite} from "../suites/CreditFacadeTestSuite.sol"; -import {CreditConfig} from "../config/CreditConfig.sol"; +import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; +import {Tokens} from "../../config/Tokens.sol"; +import {CreditFacadeTestSuite} from "../../suites/CreditFacadeTestSuite.sol"; +import {CreditConfig} from "../../config/CreditConfig.sol"; import {Test} from "forge-std/Test.sol"; uint256 constant WETH_TEST_AMOUNT = 5 * WAD; @@ -117,7 +117,7 @@ contract CreditFacadeGasTest is uint256 collateralTokensCount = creditManager.collateralTokensCount(); for (uint256 i = 0; i < collateralTokensCount; ++i) { - (address token,) = creditManager.collateralTokens(i); + (address token,) = creditManager.collateralTokensByMask(1 << i); vm.prank(address(creditConfigurator)); CreditManagerV3(address(creditManager)).setCollateralTokenData(token, 0, 0, type(uint40).max, 0); @@ -130,7 +130,7 @@ contract CreditFacadeGasTest is /// /// - /// @dev [G-FA-2]: openCreditAccount with just adding collateral + /// @dev G:[FA-2]: openCreditAccount with just adding collateral function test_G_FA_02_openCreditAccountMulticall_gas_estimate_1() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); @@ -146,7 +146,7 @@ contract CreditFacadeGasTest is uint256 gasBefore = gasleft(); vm.prank(USER); - creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); uint256 gasSpent = gasBefore - gasleft(); @@ -154,7 +154,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-3]: openCreditAccount with adding collateral and single swap + /// @dev G:[FA-3]: openCreditAccount with adding collateral and single swap function test_G_FA_03_openCreditAccountMulticall_gas_estimate_2() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); @@ -178,7 +178,7 @@ contract CreditFacadeGasTest is uint256 gasBefore = gasleft(); vm.prank(USER); - creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); uint256 gasSpent = gasBefore - gasleft(); @@ -188,7 +188,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-4]: openCreditAccount with adding collateral and two swaps + /// @dev G:[FA-4]: openCreditAccount with adding collateral and two swaps function test_G_FA_04_openCreditAccountMulticall_gas_estimate_3() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); @@ -220,7 +220,7 @@ contract CreditFacadeGasTest is uint256 gasBefore = gasleft(); vm.prank(USER); - creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); uint256 gasSpent = gasBefore - gasleft(); @@ -230,7 +230,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-5]: openCreditAccount with adding quoted collateral and updating quota + /// @dev G:[FA-5]: openCreditAccount with adding quoted collateral and updating quota function test_G_FA_05_openCreditAccountMulticall_gas_estimate_4() public { vm.startPrank(CONFIGURATOR); cft.gaugeMock().addQuotaToken(tokenTestSuite.addressOf(Tokens.LINK), 500); @@ -288,7 +288,7 @@ contract CreditFacadeGasTest is uint256 gasBefore = gasleft(); vm.prank(USER); - creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); uint256 gasSpent = gasBefore - gasleft(); @@ -296,7 +296,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-6]: openCreditAccount with swapping and updating quota + /// @dev G:[FA-6]: openCreditAccount with swapping and updating quota function test_G_FA_06_openCreditAccountMulticall_gas_estimate_5() public { vm.startPrank(CONFIGURATOR); cft.gaugeMock().addQuotaToken(tokenTestSuite.addressOf(Tokens.LINK), 500); @@ -336,7 +336,7 @@ contract CreditFacadeGasTest is uint256 gasBefore = gasleft(); vm.prank(USER); - creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); uint256 gasSpent = gasBefore - gasleft(); @@ -346,7 +346,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-7]: multicall with increaseDebt + /// @dev G:[FA-7]: multicall with increaseDebt function test_G_FA_07_increaseDebt_gas_estimate_1() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); @@ -360,7 +360,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); calls[0] = MultiCall({ target: address(creditFacade), @@ -378,7 +378,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-8]: multicall with decreaseDebt + /// @dev G:[FA-8]: multicall with decreaseDebt function test_G_FA_08_decreaseDebt_gas_estimate_1() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); @@ -392,7 +392,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); calls[0] = MultiCall({ target: address(creditFacade), @@ -410,7 +410,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-9]: multicall with decreaseDebt and active quota interest + /// @dev G:[FA-9]: multicall with decreaseDebt and active quota interest function test_G_FA_09_decreaseDebt_gas_estimate_2() public { vm.startPrank(CONFIGURATOR); cft.gaugeMock().addQuotaToken(tokenTestSuite.addressOf(Tokens.LINK), 500); @@ -440,7 +440,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); vm.warp(block.timestamp + 30 days); @@ -460,7 +460,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-10]: multicall with enableToken + /// @dev G:[FA-10]: multicall with enableToken function test_G_FA_10_enableToken_gas_estimate_1() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); @@ -474,7 +474,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); calls[0] = MultiCall({ target: address(creditFacade), @@ -492,7 +492,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-11]: multicall with disableToken + /// @dev G:[FA-11]: multicall with disableToken function test_G_FA_11_disableToken_gas_estimate_1() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); @@ -511,7 +511,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); calls[0] = MultiCall({ target: address(creditFacade), @@ -529,7 +529,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-12]: multicall with a single swap + /// @dev G:[FA-12]: multicall with a single swap function test_G_FA_12_multicall_gas_estimate_1() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); @@ -543,7 +543,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); calls[0] = MultiCall({ target: address(adapterMock), @@ -564,7 +564,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-12A]: multicall with a single swap + /// @dev G:[FA-12A]: multicall with a single swap function test_G_FA_12A_multicall_gas_estimate_1A() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); @@ -578,7 +578,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); calls = new MultiCall[](2); @@ -609,7 +609,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-13]: multicall with a single swap into quoted token + /// @dev G:[FA-13]: multicall with a single swap into quoted token function test_G_FA_13_multicall_gas_estimate_2() public { vm.startPrank(CONFIGURATOR); cft.gaugeMock().addQuotaToken(tokenTestSuite.addressOf(Tokens.LINK), 500); @@ -630,7 +630,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); tokenTestSuite.burn(Tokens.DAI, creditAccount, DAI_ACCOUNT_AMOUNT * 2); tokenTestSuite.mint(Tokens.LINK, creditAccount, LINK_ACCOUNT_AMOUNT * 3); @@ -649,7 +649,7 @@ contract CreditFacadeGasTest is target: address(creditFacade), callData: abi.encodeCall( ICreditFacadeMulticall.updateQuota, - (tokenTestSuite.addressOf(Tokens.LINK), int96(int256(LINK_ACCOUNT_AMOUNT))) + (tokenTestSuite.addressOf(Tokens.LINK), int96(int256(LINK_ACCOUNT_AMOUNT * 3))) ) }); @@ -670,7 +670,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-14]: closeCreditAccount with underlying only + /// @dev G:[FA-14]: closeCreditAccount with underlying only function test_G_FA_14_closeCreditAccount_gas_estimate_1() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); @@ -684,7 +684,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); vm.roll(block.number + 1); @@ -701,7 +701,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-15]: closeCreditAccount with two tokens + /// @dev G:[FA-15]: closeCreditAccount with two tokens function test_G_FA_15_closeCreditAccount_gas_estimate_2() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); tokenTestSuite.mint(Tokens.LINK, USER, LINK_ACCOUNT_AMOUNT); @@ -724,7 +724,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); vm.roll(block.number + 1); @@ -741,7 +741,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-16]: closeCreditAccount with 2 tokens and active quota interest + /// @dev G:[FA-16]: closeCreditAccount with 2 tokens and active quota interest function test_G_FA_16_closeCreditAccount_gas_estimate_3() public { vm.startPrank(CONFIGURATOR); cft.gaugeMock().addQuotaToken(tokenTestSuite.addressOf(Tokens.LINK), 500); @@ -771,7 +771,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); vm.roll(block.number + 1); @@ -790,7 +790,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-17]: closeCreditAccount with one swap + /// @dev G:[FA-17]: closeCreditAccount with one swap function test_G_FA_17_closeCreditAccount_gas_estimate_4() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); @@ -804,7 +804,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); vm.roll(block.number + 1); @@ -827,7 +827,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-18]: liquidateCreditAccount with underlying only + /// @dev G:[FA-18]: liquidateCreditAccount with underlying only function test_G_FA_18_liquidateCreditAccount_gas_estimate_1() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); @@ -841,7 +841,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); vm.roll(block.number + 1); @@ -860,7 +860,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-19]: liquidateCreditAccount with two tokens + /// @dev G:[FA-19]: liquidateCreditAccount with two tokens function test_G_FA_19_liquidateCreditAccount_gas_estimate_2() public { tokenTestSuite.mint(underlying, USER, DAI_ACCOUNT_AMOUNT); tokenTestSuite.mint(Tokens.LINK, USER, LINK_ACCOUNT_AMOUNT); @@ -886,7 +886,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); vm.roll(block.number + 1); @@ -905,7 +905,7 @@ contract CreditFacadeGasTest is emit log_uint(gasSpent); } - /// @dev [G-FA-20]: liquidateCreditAccount with 2 tokens and active quota interest + /// @dev G:[FA-20]: liquidateCreditAccount with 2 tokens and active quota interest function test_G_FA_20_liquidateCreditAccount_gas_estimate_3() public { vm.startPrank(CONFIGURATOR); cft.gaugeMock().addQuotaToken(tokenTestSuite.addressOf(Tokens.LINK), 500); @@ -938,7 +938,7 @@ contract CreditFacadeGasTest is }); vm.prank(USER); - address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, false, 0); + address creditAccount = creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, calls, 0); _zeroAllLTs(); diff --git a/contracts/test/helpers/BalanceEngine.sol b/contracts/test/helpers/BalanceEngine.sol index f4e99ef5..dab77639 100644 --- a/contracts/test/helpers/BalanceEngine.sol +++ b/contracts/test/helpers/BalanceEngine.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/test/helpers/BalanceHelper.sol b/contracts/test/helpers/BalanceHelper.sol index 2679c793..70a8bf0a 100644 --- a/contracts/test/helpers/BalanceHelper.sol +++ b/contracts/test/helpers/BalanceHelper.sol @@ -1,12 +1,40 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {TokensTestSuite} from "../suites/TokensTestSuite.sol"; import {Tokens} from "../config/Tokens.sol"; import {BalanceEngine} from "./BalanceEngine.sol"; +import "forge-std/Vm.sol"; +import "forge-std/console.sol"; + +enum Assertion { + EQUAL, + IN_RANGE, + GE, + LE +} + +struct ExpectedTokenTransfer { + string reason; + address token; + address from; + address to; + uint256 amount; + Assertion assertion; + uint256 lowerBound; + uint256 upperBound; +} + +struct TransferCheck { + uint256 amount; + bool exists; + bool used; +} /// @title CreditManagerTestSuite /// @notice Deploys contract for unit testing of CreditManagerV3.sol @@ -14,6 +42,110 @@ contract BalanceHelper is BalanceEngine { // Suites TokensTestSuite internal tokenTestSuite; + uint256 internal tokenTrackingSession = 1; + mapping(uint256 => mapping(address => mapping(address => mapping(address => TransferCheck)))) internal + _transfersCheck; + + mapping(uint256 => ExpectedTokenTransfer[]) internal _expectedTransfers; + + string private caseName; + + function startTokenTrackingSession(string memory _caseName) internal { + tokenTrackingSession++; + vm.recordLogs(); + caseName = _caseName; + } + + function expectTokenTransfer(address token, address from, address to, uint256 amount) internal { + expectTokenTransfer({reason: "", token: token, from: from, to: to, amount: amount}); + } + + function expectTokenTransfer(address token, address from, address to, uint256 amount, string memory reason) + internal + { + _expectedTransfers[tokenTrackingSession].push( + ExpectedTokenTransfer({ + reason: reason, + token: token, + from: from, + to: to, + amount: amount, + assertion: Assertion.EQUAL, + lowerBound: 0, + upperBound: 0 + }) + ); + } + + function checkTokenTransfers(bool debug) internal { + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + uint256 len = entries.length; + + if (debug) { + console.log("\n*** ", caseName, " ***\n"); + console.log("Transfers found:\n"); + } + uint256 j = 1; + for (uint256 i; i < len; ++i) { + Vm.Log memory entry = entries[i]; + + if (entry.topics[0] == keccak256("Transfer(address,address,uint256)")) { + address token = entry.emitter; + address from = address(uint160(uint256(entry.topics[1]))); + address to = address(uint160(uint256(entry.topics[2]))); + uint256 amount = abi.decode(entry.data, (uint256)); + + _transfersCheck[tokenTrackingSession][token][from][to] = + TransferCheck({amount: amount, exists: true, used: false}); + + if (debug) { + console.log("%s. %s => %s", j, from, to); + console.log(" %s %s\n", amount, IERC20Metadata(token).symbol()); + + j++; + } + } + } + + len = _expectedTransfers[tokenTrackingSession].length; + for (uint256 i; i < len; ++i) { + ExpectedTokenTransfer storage et = _expectedTransfers[tokenTrackingSession][i]; + address token = et.token; + TransferCheck storage tc = _transfersCheck[tokenTrackingSession][token][et.from][et.to]; + + if ((!tc.exists) || tc.used) { + _consoleErr(et); + assertTrue(tc.exists, "Transfer not found!"); + assertTrue(!tc.used, "Transfer was called twice!"); + } else if (et.assertion == Assertion.EQUAL && et.amount != tc.amount) { + _consoleErr(et); + assertEq(tc.amount, et.amount, "Amounts are different"); + } else if (et.assertion == Assertion.IN_RANGE && (tc.amount < et.lowerBound || tc.amount > et.upperBound)) { + _consoleErr(et); + assertLt(tc.amount, et.lowerBound, "Amount less than lower bound"); + assertGt(tc.amount, et.upperBound, "Amount greater than upper bound"); + } else if (et.assertion == Assertion.GE && (tc.amount < et.amount)) { + _consoleErr(et); + assertGe(tc.amount, et.amount, "Amount greater than expected"); + } else if (et.assertion == Assertion.LE && (tc.amount > et.amount)) { + _consoleErr(et); + assertLe(tc.amount, et.amount, "Amount less than expected"); + } else { + if (debug) console.log("[ PASS ]", et.reason); + } + + tc.used = true; + } + } + + function _consoleErr(ExpectedTokenTransfer storage et) internal view { + console.log("Case: ", caseName); + console.log(et.reason); + console.log("Problem with transfer of %s (%s)", IERC20Metadata(et.token).symbol(), et.token); + console.log(" %s => %s", et.from, et.to); + } + modifier withTokenSuite() { require(address(tokenTestSuite) != address(0), "tokenTestSuite is not set"); _; diff --git a/contracts/test/helpers/CreditFacadeTestHelper.sol b/contracts/test/helpers/CreditFacadeTestHelper.sol index 7b032167..08d3f413 100644 --- a/contracts/test/helpers/CreditFacadeTestHelper.sol +++ b/contracts/test/helpers/CreditFacadeTestHelper.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {TokensTestSuite} from "../suites/TokensTestSuite.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -53,7 +53,6 @@ contract CreditFacadeTestHelper is TestHelper { callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, amount)) }) ), - false, referralCode ); } @@ -94,7 +93,7 @@ contract CreditFacadeTestHelper is TestHelper { // switch to new block to be able to close account vm.roll(block.number + 1); - // (,, uint256 underlyingToClose) = creditManager.calcCreditAccountAccruedInterest(creditAccount); + // (,, uint256 underlyingToClose) = creditManager.calcAccruedInterestAndFees(creditAccount); // uint256 underlyingBalance = cft.tokenTestSuite().balanceOf(underlying, creditAccount); // if (underlyingToClose > underlyingBalance) { @@ -183,7 +182,7 @@ contract CreditFacadeTestHelper is TestHelper { function expectSafeAllowance(address creditAccount, address target) internal { uint256 len = creditManager.collateralTokensCount(); for (uint256 i = 0; i < len; i++) { - (address token,) = creditManager.collateralTokens(i); + (address token,) = creditManager.collateralTokensByMask(1 << i); assertLe(IERC20(token).allowance(creditAccount, target), 1, "allowance is too high"); } } diff --git a/contracts/test/unit/credit/CreditConfigurator.t.sol b/contracts/test/integration/credit/CreditConfigurator.int.t.sol similarity index 82% rename from contracts/test/unit/credit/CreditConfigurator.t.sol rename to contracts/test/integration/credit/CreditConfigurator.int.t.sol index c83d9625..39b56a87 100644 --- a/contracts/test/unit/credit/CreditConfigurator.t.sol +++ b/contracts/test/integration/credit/CreditConfigurator.int.t.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; +import "../../../interfaces/IAddressProviderV3.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {CreditFacadeV3} from "../../../credit/CreditFacadeV3.sol"; import {CreditManagerV3} from "../../../credit/CreditManagerV3.sol"; @@ -27,7 +28,7 @@ import "../../../interfaces/IExceptions.sol"; import "../../lib/constants.sol"; // MOCKS -import {AdapterMock} from "../../mocks/adapters/AdapterMock.sol"; +import {AdapterMock} from "../../mocks//adapters/AdapterMock.sol"; import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/adapters/TargetContractMock.sol"; // SUITES @@ -40,9 +41,7 @@ import {CollateralTokensItem} from "../../config/CreditConfig.sol"; import {Test} from "forge-std/Test.sol"; -/// @title CreditConfiguratorTest -/// @notice Designed for unit test purposes only -contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfiguratorEvents { +contract CreditConfiguratorIntegrationTest is Test, ICreditManagerV3Events, ICreditConfiguratorEvents { using AddressList for address[]; TokensTestSuite tokenTestSuite; @@ -156,8 +155,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur /// /// - /// @dev [CC-1]: constructor sets correct values - function test_CC_01_constructor_sets_correct_values() public { + /// @dev I:[CC-1]: constructor sets correct values + function test_I_CC_01_constructor_sets_correct_values() public { assertEq(address(creditConfigurator.creditManager()), address(creditManager), "Incorrect creditManager"); assertEq(address(creditConfigurator.creditFacade()), address(creditFacade), "Incorrect creditFacade"); @@ -213,7 +212,7 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur assertEq(creditManager.collateralTokensCount(), len, "Incorrect quantity of allowed tokens"); for (uint256 i = 0; i < len; i++) { - (address token, uint16 lt) = creditManager.collateralTokens(i); + (address token, uint16 lt) = creditManager.collateralTokensByMask(1 << i); assertEq(token, tokenTestSuite.addressOf(collateralTokenOpts[i].token), "Incorrect token address"); @@ -240,8 +239,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur assertEq(expirationDate, 0, "Incorrect expiration date"); } - /// @dev [CC-1A]: constructor emits all events - function test_CC_01A_constructor_emits_all_events() public { + /// @dev I:[CC-1A]: constructor emits all events + function test_I_CC_01A_constructor_emits_all_events() public { CollateralToken[] memory cTokens = new CollateralToken[](1); cTokens[0] = CollateralToken({token: tokenTestSuite.addressOf(Tokens.USDC), liquidationThreshold: 6000}); @@ -255,7 +254,7 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur expirable: false }); - creditManager = new CreditManagerV3(address(cct.poolMock()), address(withdrawalManager)); + creditManager = new CreditManagerV3(address(cct.addressProvider()), address(cct.poolMock())); creditFacade = new CreditFacadeV3( address(creditManager), creditOpts.degenNFT, @@ -307,8 +306,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur _deploy(configuratorByteCode, 0); } - /// @dev [CC-2]: all functions revert if called non-configurator - function test_CC_02_all_functions_revert_if_called_non_configurator() public { + /// @dev I:[CC-2]: all functions revert if called non-configurator + function test_I_CC_02_all_functions_revert_if_called_non_configurator() public { vm.startPrank(USER); // Token mgmt @@ -331,7 +330,7 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur // Upgrades vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.setPriceOracle(); + creditConfigurator.setPriceOracle(0); vm.expectRevert(CallerNotConfiguratorException.selector); creditConfigurator.setCreditFacade(DUMB_ADDRESS, false); @@ -340,7 +339,7 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur creditConfigurator.upgradeCreditConfigurator(DUMB_ADDRESS); vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.setBotList(FRIEND); + creditConfigurator.setBotList(0); vm.expectRevert(CallerNotConfiguratorException.selector); creditConfigurator.setMaxCumulativeLoss(0); @@ -351,7 +350,7 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur vm.stopPrank(); } - function test_CC_02A_forbidBorrowing_on_non_pausable_admin() public { + function test_I_CC_02A_forbidBorrowing_on_non_pausable_admin() public { vm.expectRevert(CallerNotPausableAdminException.selector); creditConfigurator.forbidBorrowing(); @@ -359,7 +358,7 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur creditConfigurator.forbidBorrowing(); } - function test_CC_02B_controllerOnly_functions_revert_on_non_controller() public { + function test_I_CC_02B_controllerOnly_functions_revert_on_non_controller() public { vm.expectRevert(CallerNotControllerException.selector); creditConfigurator.setLiquidationThreshold(DUMB_ADDRESS, uint16(0)); @@ -379,15 +378,15 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur creditConfigurator.setMaxEnabledTokens(1); vm.expectRevert(CallerNotControllerException.selector); - creditConfigurator.rampLiquidationThreshold(DUMB_ADDRESS, 0, 0); + creditConfigurator.rampLiquidationThreshold(DUMB_ADDRESS, 0, 0, 0); } // // TOKEN MANAGEMENT // - /// @dev [CC-3]: addCollateralToken reverts for zero address or in priceFeed - function test_CC_03_addCollateralToken_reverts_for_zero_address_or_in_priceFeed() public { + /// @dev I:[CC-3]: addCollateralToken reverts for zero address or in priceFeed + function test_I_CC_03_addCollateralToken_reverts_for_zero_address_or_in_priceFeed() public { vm.startPrank(CONFIGURATOR); vm.expectRevert(ZeroAddressException.selector); @@ -407,8 +406,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur vm.stopPrank(); } - /// @dev [CC-4]: addCollateralToken adds new token to creditManager - function test_CC_04_addCollateralToken_adds_new_token_to_creditManager_and_set_lt() public { + /// @dev I:[CC-4]: addCollateralToken adds new token to creditManager + function test_I_CC_04_addCollateralToken_adds_new_token_to_creditManager_and_set_lt() public { uint256 tokensCountBefore = creditManager.collateralTokensCount(); address cLINKToken = tokenTestSuite.addressOf(Tokens.LUNA); @@ -421,7 +420,7 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur assertEq(creditManager.collateralTokensCount(), tokensCountBefore + 1, "Incorrect tokens count"); - (address token,) = creditManager.collateralTokens(tokensCountBefore); + (address token,) = creditManager.collateralTokensByMask(1 << tokensCountBefore); assertEq(token, cLINKToken, "Token is not added to list"); @@ -430,8 +429,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur assertEq(creditManager.liquidationThresholds(cLINKToken), 8800, "Threshold wasn't set"); } - /// @dev [CC-5]: setLiquidationThreshold reverts for underling token and incorrect values - function test_CC_05_setLiquidationThreshold_reverts_for_underling_token_and_incorrect_values() public { + /// @dev I:[CC-5]: setLiquidationThreshold reverts for underling token and incorrect values + function test_I_CC_05_setLiquidationThreshold_reverts_for_underling_token_and_incorrect_values() public { vm.startPrank(CONFIGURATOR); vm.expectRevert(SetLTForUnderlyingException.selector); @@ -446,8 +445,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur vm.stopPrank(); } - /// @dev [CC-6]: setLiquidationThreshold sets liquidation threshold in creditManager - function test_CC_06_setLiquidationThreshold_sets_liquidation_threshold_in_creditManager() public { + /// @dev I:[CC-6]: setLiquidationThreshold sets liquidation threshold in creditManager + function test_I_CC_06_setLiquidationThreshold_sets_liquidation_threshold_in_creditManager() public { address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); uint16 newLT = 24; @@ -460,8 +459,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur assertEq(creditManager.liquidationThresholds(usdcToken), newLT); } - /// @dev [CC-7]: allowToken and forbidToken reverts for unknown or underlying token - function test_CC_07_allowToken_and_forbidToken_reverts_for_unknown_or_underlying_token() public { + /// @dev I:[CC-7]: allowToken and forbidToken reverts for unknown or underlying token + function test_I_CC_07_allowToken_and_forbidToken_reverts_for_unknown_or_underlying_token() public { vm.startPrank(CONFIGURATOR); vm.expectRevert(TokenNotAllowedException.selector); @@ -479,8 +478,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur vm.stopPrank(); } - /// @dev [CC-8]: allowToken doesn't change forbidden mask if its already allowed - function test_CC_08_allowToken_doesnt_change_forbidden_mask_if_its_already_allowed() public { + /// @dev I:[CC-8]: allowToken doesn't change forbidden mask if its already allowed + function test_I_CC_08_allowToken_doesnt_change_forbidden_mask_if_its_already_allowed() public { address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); uint256 forbiddenMask = creditFacade.forbiddenTokenMask(); @@ -492,8 +491,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur // TODO: change tests - // /// @dev [CC-9]: allowToken allows token if it was forbidden - // function test_CC_09_allows_token_if_it_was_forbidden() public { + // /// @dev I:[CC-9]: allowToken allows token if it was forbidden + // function test_I_CC_09_allows_token_if_it_was_forbidden() public { // address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); // uint256 tokenMask = creditManager.getTokenMaskOrRevert(usdcToken); @@ -509,8 +508,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur // assertEq(creditManager.forbiddenTokenMask(), 0, "Incorrect forbidden mask"); // } - // /// @dev [CC-10]: forbidToken doesn't change forbidden mask if its already forbidden - // function test_CC_10_forbidToken_doesnt_change_forbidden_mask_if_its_already_forbidden() public { + // /// @dev I:[CC-10]: forbidToken doesn't change forbidden mask if its already forbidden + // function test_I_CC_10_forbidToken_doesnt_change_forbidden_mask_if_its_already_forbidden() public { // address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); // uint256 tokenMask = creditManager.getTokenMaskOrRevert(usdcToken); @@ -525,8 +524,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur // assertEq(creditManager.forbiddenTokenMask(), forbiddenMask, "Incorrect forbidden mask"); // } - // /// @dev [CC-11]: forbidToken forbids token and enable IncreaseDebtForbidden mode if it was allowed - // function test_CC_11_forbidToken_forbids_token_if_it_was_allowed() public { + // /// @dev I:[CC-11]: forbidToken forbids token and enable IncreaseDebtForbidden mode if it was allowed + // function test_I_CC_11_forbidToken_forbids_token_if_it_was_allowed() public { // address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); // uint256 tokenMask = creditManager.getTokenMaskOrRevert(usdcToken); @@ -546,8 +545,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur // CONFIGURATION: CONTRACTS & ADAPTERS MANAGEMENT // - /// @dev [CC-12]: allowContract and forbidContract reverts for zero address - function test_CC_12_allowContract_and_forbidContract_reverts_for_zero_address() public { + /// @dev I:[CC-12]: allowContract and forbidContract reverts for zero address + function test_I_CC_12_allowContract_and_forbidContract_reverts_for_zero_address() public { vm.startPrank(CONFIGURATOR); vm.expectRevert(ZeroAddressException.selector); @@ -562,8 +561,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur vm.stopPrank(); } - /// @dev [CC-12A]: allowContract reverts for non contract addresses - function test_CC_12A_allowContract_reverts_for_non_contract_addresses() public { + /// @dev I:[CC-12A]: allowContract reverts for non contract addresses + function test_I_CC_12A_allowContract_reverts_for_non_contract_addresses() public { vm.startPrank(CONFIGURATOR); vm.expectRevert(abi.encodeWithSelector(AddressIsNotContractException.selector, DUMB_ADDRESS)); @@ -575,8 +574,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur vm.stopPrank(); } - /// @dev [CC-12B]: allowContract reverts for non compartible adapter contract - function test_CC_12B_allowContract_reverts_for_non_compartible_adapter_contract() public { + /// @dev I:[CC-12B]: allowContract reverts for non compartible adapter contract + function test_I_CC_12B_allowContract_reverts_for_non_compartible_adapter_contract() public { vm.startPrank(CONFIGURATOR); // Should be reverted, cause undelring token has no .creditManager() method @@ -590,8 +589,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur vm.stopPrank(); } - /// @dev [CC-13]: allowContract reverts for creditManager and creditFacade contracts - function test_CC_13_allowContract_reverts_for_creditManager_and_creditFacade_contracts() public { + /// @dev I:[CC-13]: allowContract reverts for creditManager and creditFacade contracts + function test_I_CC_13_allowContract_reverts_for_creditManager_and_creditFacade_contracts() public { vm.startPrank(CONFIGURATOR); vm.expectRevert(TargetContractNotAllowedException.selector); @@ -606,8 +605,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur vm.stopPrank(); } - /// @dev [CC-14]: allowContract: adapter could not be used twice - function test_CC_14_allowContract_adapter_cannot_be_used_twice() public { + /// @dev I:[CC-14]: allowContract: adapter could not be used twice + function test_I_CC_14_allowContract_adapter_cannot_be_used_twice() public { vm.startPrank(CONFIGURATOR); creditConfigurator.allowContract(DUMB_COMPARTIBLE_CONTRACT, address(adapter1)); @@ -618,8 +617,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur vm.stopPrank(); } - /// @dev [CC-15]: allowContract allows targetContract <-> adapter and emits event - function test_CC_15_allowContract_allows_targetContract_adapter_and_emits_event() public { + /// @dev I:[CC-15]: allowContract allows targetContract <-> adapter and emits event + function test_I_CC_15_allowContract_allows_targetContract_adapter_and_emits_event() public { address[] memory allowedContracts = creditConfigurator.allowedContracts(); uint256 allowedContractCount = allowedContracts.length; @@ -647,8 +646,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur assertTrue(allowedContracts.includes(TARGET_CONTRACT), "Target contract wasnt found"); } - // /// @dev [CC-15A]: allowContract allows universal adapter for universal contract - // function test_CC_15A_allowContract_allows_universal_contract() public { + // /// @dev I:[CC-15A]: allowContract allows universal adapter for universal contract + // function test_I_CC_15A_allowContract_allows_universal_contract() public { // vm.prank(CONFIGURATOR); // vm.expectEmit(true, true, false, false); @@ -659,8 +658,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur // assertEq(creditManager.universalAdapter(), address(adapter1), "Universal adapter wasn't updated"); // } - /// @dev [CC-15A]: allowContract removes existing adapter - function test_CC_15A_allowContract_removes_old_adapter_if_it_exists() public { + /// @dev I:[CC-15A]: allowContract removes existing adapter + function test_I_CC_15A_allowContract_removes_old_adapter_if_it_exists() public { vm.prank(CONFIGURATOR); creditConfigurator.allowContract(TARGET_CONTRACT, address(adapter1)); @@ -683,16 +682,16 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur assertEq(creditManager.adapterToContract(address(adapter1)), address(0), "Old adapter was not removed"); } - /// @dev [CC-16]: forbidContract reverts for unknown contract - function test_CC_16_forbidContract_reverts_for_unknown_contract() public { + /// @dev I:[CC-16]: forbidContract reverts for unknown contract + function test_I_CC_16_forbidContract_reverts_for_unknown_contract() public { vm.expectRevert(ContractIsNotAnAllowedAdapterException.selector); vm.prank(CONFIGURATOR); creditConfigurator.forbidContract(TARGET_CONTRACT); } - /// @dev [CC-17]: forbidContract forbids contract and emits event - function test_CC_17_forbidContract_forbids_contract_and_emits_event() public { + /// @dev I:[CC-17]: forbidContract forbids contract and emits event + function test_I_CC_17_forbidContract_forbids_contract_and_emits_event() public { vm.startPrank(CONFIGURATOR); creditConfigurator.allowContract(DUMB_COMPARTIBLE_CONTRACT, address(adapter1)); @@ -727,8 +726,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur // CREDIT MANAGER MGMT // - /// @dev [CC-18]: setLimits reverts if minAmount > maxAmount - function test_CC_18_setLimits_reverts_if_minAmount_gt_maxAmount() public { + /// @dev I:[CC-18]: setLimits reverts if minAmount > maxAmount + function test_I_CC_18_setLimits_reverts_if_minAmount_gt_maxAmount() public { (uint128 minBorrowedAmount, uint128 maxBorrowedAmount) = creditFacade.debtLimits(); vm.expectRevert(IncorrectLimitsException.selector); @@ -737,8 +736,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur creditConfigurator.setLimits(maxBorrowedAmount, minBorrowedAmount); } - /// @dev [CC-19]: setLimits sets limits - function test_CC_19_setLimits_sets_limits() public { + /// @dev I:[CC-19]: setLimits sets limits + function test_I_CC_19_setLimits_sets_limits() public { (uint128 minBorrowedAmount, uint128 maxBorrowedAmount) = creditFacade.debtLimits(); uint128 newMinBorrowedAmount = minBorrowedAmount + 1000; uint128 newMaxBorrowedAmount = maxBorrowedAmount + 1000; @@ -752,8 +751,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur assertEq(maxBorrowedAmount, newMaxBorrowedAmount, "Incorrect maxBorrowedAmount"); } - /// @dev [CC-23]: setFees reverts for incorrect fees - function test_CC_23_setFees_reverts_for_incorrect_fees() public { + /// @dev I:[CC-23]: setFees reverts for incorrect fees + function test_I_CC_23_setFees_reverts_for_incorrect_fees() public { (, uint16 feeLiquidation,, uint16 feeLiquidationExpired,) = creditManager.fees(); vm.expectRevert(IncorrectParameterException.selector); @@ -778,8 +777,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur ); } - /// @dev [CC-25]: setFees updates LT for underlying and for all tokens which bigger than new LT - function test_CC_25_setFees_updates_LT_for_underlying_and_for_all_tokens_which_bigger_than_new_LT() public { + /// @dev I:[CC-25]: setFees updates LT for underlying and for all tokens which bigger than new LT + function test_I_CC_25_setFees_updates_LT_for_underlying_and_for_all_tokens_which_bigger_than_new_LT() public { vm.startPrank(CONFIGURATOR); (uint16 feeInterest,,,,) = creditManager.fees(); @@ -813,8 +812,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur assertEq(creditManager.liquidationThresholds(wethToken), wethLTBefore, "Incorrect WETH for underlying token"); } - /// @dev [CC-26]: setFees sets fees and doesn't change others - function test_CC_26_setFees_sets_fees_and_doesnt_change_others() public { + /// @dev I:[CC-26]: setFees sets fees and doesn't change others + function test_I_CC_26_setFees_sets_fees_and_doesnt_change_others() public { ( uint16 feeInterest, uint16 feeLiquidation, @@ -860,22 +859,22 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur // CONTRACT UPGRADES // - /// @dev [CC-28]: setPriceOracle upgrades priceOracleCorrectly and doesnt change facade - function test_CC_28_setPriceOracle_upgrades_priceOracleCorrectly_and_doesnt_change_facade() public { + /// @dev I:[CC-28]: setPriceOracle upgrades priceOracleCorrectly and doesnt change facade + function test_I_CC_28_setPriceOracle_upgrades_priceOracleCorrectly_and_doesnt_change_facade() public { vm.startPrank(CONFIGURATOR); - cct.addressProvider().setPriceOracle(DUMB_ADDRESS); + cct.addressProvider().setAddress(AP_PRICE_ORACLE, DUMB_ADDRESS, false); vm.expectEmit(true, false, false, false); emit SetPriceOracle(DUMB_ADDRESS); - creditConfigurator.setPriceOracle(); + creditConfigurator.setPriceOracle(0); assertEq(address(creditManager.priceOracle()), DUMB_ADDRESS); vm.stopPrank(); } - /// @dev [CC-29]: setPriceOracle upgrades priceOracleCorrectly and doesnt change facade - function test_CC_29_setCreditFacade_upgradeCreditConfigurator_reverts_for_incompatible_contracts() public { + /// @dev I:[CC-29]: setPriceOracle upgrades priceOracleCorrectly and doesnt change facade + function test_I_CC_29_setCreditFacade_upgradeCreditConfigurator_reverts_for_incompatible_contracts() public { vm.startPrank(CONFIGURATOR); vm.expectRevert(ZeroAddressException.selector); @@ -903,8 +902,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur creditConfigurator.upgradeCreditConfigurator(address(adapterDifferentCM)); } - /// @dev [CC-30]: setCreditFacade upgrades creditFacade and doesnt change priceOracle - function test_CC_30_setCreditFacade_upgrades_creditFacade_and_doesnt_change_priceOracle() public { + /// @dev I:[CC-30]: setCreditFacade upgrades creditFacade and doesnt change priceOracle + function test_I_CC_30_setCreditFacade_upgrades_creditFacade_and_doesnt_change_priceOracle() public { for (uint256 ex = 0; ex < 2; ex++) { bool isExpirable = ex != 0; for (uint256 ms = 0; ms < 2; ms++) { @@ -946,7 +945,9 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur vm.prank(CONFIGURATOR); creditConfigurator.setCreditFacade(address(cf), migrateSettings); - assertEq(address(creditManager.priceOracle()), cct.addressProvider().getPriceOracle()); + assertEq( + address(creditManager.priceOracle()), cct.addressProvider().getAddressOrRevert(AP_PRICE_ORACLE, 2) + ); assertEq(address(creditManager.creditFacade()), address(cf)); assertEq(address(creditConfigurator.creditFacade()), address(cf)); @@ -970,17 +971,14 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur } } - /// @dev [CC-30A]: usetCreditFacade transfers bot list - function test_CC_30A_botList_is_transferred_on_CreditFacade_upgrade() public { + /// @dev I:[CC-30A]: usetCreditFacade transfers bot list + function test_I_CC_30A_botList_is_transferred_on_CreditFacade_upgrade() public { for (uint256 ms = 0; ms < 2; ms++) { bool migrateSettings = ms != 0; setUp(); - address botList = address(new BotList(address(cct.addressProvider()))); - - vm.prank(CONFIGURATOR); - creditConfigurator.setBotList(botList); + address botList = creditFacade.botList(); CreditFacadeV3 cf = new CreditFacadeV3( address(creditManager), @@ -997,8 +995,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur } } - /// @dev [CC-31]: uupgradeCreditConfigurator upgrades creditConfigurator - function test_CC_31_upgradeCreditConfigurator_upgrades_creditConfigurator() public { + /// @dev I:[CC-31]: uupgradeCreditConfigurator upgrades creditConfigurator + function test_I_CC_31_upgradeCreditConfigurator_upgrades_creditConfigurator() public { vm.expectEmit(true, false, false, false); emit CreditConfiguratorUpgraded(DUMB_COMPARTIBLE_CONTRACT); @@ -1008,8 +1006,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur assertEq(address(creditManager.creditConfigurator()), DUMB_COMPARTIBLE_CONTRACT); } - /// @dev [CC-32]: setBorrowingAllowance sets IncreaseDebtForbidden - function test_CC_32_setBorrowingAllowance_sets_IncreaseDebtForbidden() public { + /// @dev I:[CC-32]: setBorrowingAllowance sets IncreaseDebtForbidden + function test_I_CC_32_setBorrowingAllowance_sets_IncreaseDebtForbidden() public { /// TODO: Change test // for (uint256 id = 0; id < 2; id++) { // bool isIDF = id != 0; @@ -1038,8 +1036,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur // } } - /// @dev [CC-33]: setMaxDebtLimitPerBlock reverts if it lt maxLimit otherwise sets limitPerBlock - function test_CC_33_setMaxDebtLimitPerBlock_reverts_if_it_lt_maxLimit_otherwise_sets_limitPerBlock() public { + /// @dev I:[CC-33]: setMaxDebtLimitPerBlock reverts if it lt maxLimit otherwise sets limitPerBlock + function test_I_CC_33_setMaxDebtLimitPerBlock_reverts_if_it_lt_maxLimit_otherwise_sets_limitPerBlock() public { // (, uint128 maxBorrowedAmount) = creditFacade.debtLimits(); // vm.prank(CONFIGURATOR); @@ -1059,8 +1057,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur // assertEq(maxBorrowedAmountPerBlock, newLimitBlock, "Incorrect new limits block"); } - /// @dev [CC-34]: setExpirationDate reverts if the new expiration date is stale, otherwise sets it - function test_CC_34_setExpirationDate_reverts_on_incorrect_newExpirationDate_otherwise_sets() public { + /// @dev I:[CC-34]: setExpirationDate reverts if the new expiration date is stale, otherwise sets it + function test_I_CC_34_setExpirationDate_reverts_on_incorrect_newExpirationDate_otherwise_sets() public { // cct.testFacadeWithExpiration(); // creditFacade = cct.creditFacade(); @@ -1091,8 +1089,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur assertEq(expirationDate, newExpirationDate, "Incorrect new expirationDate"); } - /// @dev [CC-37]: setMaxEnabledTokens works correctly and emits event - function test_CC_37_setMaxEnabledTokens_works_correctly() public { + /// @dev I:[CC-37]: setMaxEnabledTokens works correctly and emits event + function test_I_CC_37_setMaxEnabledTokens_works_correctly() public { vm.expectRevert(CallerNotControllerException.selector); creditConfigurator.setMaxEnabledTokens(255); @@ -1102,11 +1100,11 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur vm.prank(CONFIGURATOR); creditConfigurator.setMaxEnabledTokens(255); - assertEq(creditManager.maxAllowedEnabledTokenLength(), 255, "Credit manager max enabled tokens incorrect"); + assertEq(creditManager.maxEnabledTokens(), 255, "Credit manager max enabled tokens incorrect"); } - /// @dev [CC-38]: addEmergencyLiquidator works correctly and emits event - function test_CC_38_addEmergencyLiquidator_works_correctly() public { + /// @dev I:[CC-38]: addEmergencyLiquidator works correctly and emits event + function test_I_CC_38_addEmergencyLiquidator_works_correctly() public { vm.expectRevert(CallerNotConfiguratorException.selector); creditConfigurator.addEmergencyLiquidator(DUMB_ADDRESS); @@ -1121,8 +1119,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur ); } - /// @dev [CC-39]: removeEmergencyLiquidator works correctly and emits event - function test_CC_39_removeEmergencyLiquidator_works_correctly() public { + /// @dev I:[CC-39]: removeEmergencyLiquidator works correctly and emits event + function test_I_CC_39_removeEmergencyLiquidator_works_correctly() public { vm.expectRevert(CallerNotConfiguratorException.selector); creditConfigurator.removeEmergencyLiquidator(DUMB_ADDRESS); @@ -1140,31 +1138,8 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur ); } - /// @dev [CC-40]: forbidAdapter works correctly and emits event - function test_CC_40_forbidAdapter_works_correctly() public { - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.forbidAdapter(DUMB_ADDRESS); - - vm.prank(CONFIGURATOR); - creditConfigurator.allowContract(TARGET_CONTRACT, address(adapter1)); - - vm.expectEmit(true, false, false, false); - emit ForbidAdapter(address(adapter1)); - - vm.prank(CONFIGURATOR); - creditConfigurator.forbidAdapter(address(adapter1)); - - assertEq( - creditManager.adapterToContract(address(adapter1)), address(0), "Adapter to contract link was not removed" - ); - - assertEq( - creditManager.contractToAdapter(TARGET_CONTRACT), address(adapter1), "Contract to adapter link was removed" - ); - } - - /// @dev [CC-41]: allowedContracts migrate correctly - function test_CC_41_allowedContracts_are_migrated_correctly_for_new_CC() public { + /// @dev I:[CC-41]: allowedContracts migrate correctly + function test_I_CC_41_allowedContracts_are_migrated_correctly_for_new_CC() public { vm.prank(CONFIGURATOR); creditConfigurator.allowContract(TARGET_CONTRACT, address(adapter1)); @@ -1206,17 +1181,17 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur } } - function test_CC_42_rampLiquidationThreshold_works_correctly() public { + function test_I_CC_42_rampLiquidationThreshold_works_correctly() public { address dai = tokenTestSuite.addressOf(Tokens.DAI); address usdc = tokenTestSuite.addressOf(Tokens.USDC); vm.expectRevert(SetLTForUnderlyingException.selector); vm.prank(CONFIGURATOR); - creditConfigurator.rampLiquidationThreshold(dai, 9000, 1); + creditConfigurator.rampLiquidationThreshold(dai, 9000, uint40(block.timestamp), 1); vm.expectRevert(IncorrectLiquidationThresholdException.selector); vm.prank(CONFIGURATOR); - creditConfigurator.rampLiquidationThreshold(usdc, 9999, 1); + creditConfigurator.rampLiquidationThreshold(usdc, 9999, uint40(block.timestamp), 1); uint16 initialLT = creditManager.liquidationThresholds(usdc); @@ -1231,6 +1206,6 @@ contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfigur ); vm.prank(CONFIGURATOR); - creditConfigurator.rampLiquidationThreshold(usdc, 8900, 1000); + creditConfigurator.rampLiquidationThreshold(usdc, 8900, uint40(block.timestamp), 1000); } } diff --git a/contracts/test/integration/credit/CreditConfigurator.t.sol b/contracts/test/integration/credit/CreditConfigurator.t.sol deleted file mode 100644 index f4320fa0..00000000 --- a/contracts/test/integration/credit/CreditConfigurator.t.sol +++ /dev/null @@ -1,1236 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; - -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {CreditFacadeV3} from "../../../credit/CreditFacadeV3.sol"; -import {CreditManagerV3} from "../../../credit/CreditManagerV3.sol"; -import {WithdrawalManager} from "../../../support/WithdrawalManager.sol"; -import {CreditConfigurator, CreditManagerOpts, CollateralToken} from "../../../credit/CreditConfiguratorV3.sol"; -import {ICreditManagerV3, ICreditManagerV3Events} from "../../../interfaces/ICreditManagerV3.sol"; -import {ICreditConfiguratorEvents} from "../../../interfaces/ICreditConfiguratorV3.sol"; -import {IAdapter} from "@gearbox-protocol/core-v2/contracts/interfaces/adapters/IAdapter.sol"; - -import {BotList} from "../../../support/BotList.sol"; - -// -import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; -import "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; -import {AddressList} from "@gearbox-protocol/core-v2/contracts/libraries/AddressList.sol"; - -// EXCEPTIONS - -import "../../../interfaces/IExceptions.sol"; - -// TEST -import "../../lib/constants.sol"; - -// MOCKS -import {AdapterMock} from "../../mocks/adapters/AdapterMock.sol"; -import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/adapters/TargetContractMock.sol"; - -// SUITES -import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; -import {Tokens} from "../../config/Tokens.sol"; -import {CreditFacadeTestSuite} from "../../suites/CreditFacadeTestSuite.sol"; -import {CreditConfig} from "../../config/CreditConfig.sol"; - -import {CollateralTokensItem} from "../../config/CreditConfig.sol"; - -import {Test} from "forge-std/Test.sol"; - -/// @title CreditConfiguratorTest -/// @notice Designed for unit test purposes only -contract CreditConfiguratorTest is Test, ICreditManagerV3Events, ICreditConfiguratorEvents { - using AddressList for address[]; - - TokensTestSuite tokenTestSuite; - CreditFacadeTestSuite cct; - - CreditManagerV3 public creditManager; - CreditFacadeV3 public creditFacade; - CreditConfigurator public creditConfigurator; - WithdrawalManager public withdrawalManager; - address underlying; - - AdapterMock adapter1; - AdapterMock adapterDifferentCM; - - address DUMB_COMPARTIBLE_CONTRACT; - address TARGET_CONTRACT; - - function setUp() public { - _setUp(false, false, false); - } - - function _setUp(bool withDegenNFT, bool expirable, bool supportQuotas) public { - tokenTestSuite = new TokensTestSuite(); - tokenTestSuite.topUpWETH{value: 100 * WAD}(); - - CreditConfig creditConfig = new CreditConfig( - tokenTestSuite, - Tokens.DAI - ); - - cct = new CreditFacadeTestSuite(creditConfig, withDegenNFT, expirable, supportQuotas, 1); - - underlying = cct.underlying(); - creditManager = cct.creditManager(); - creditFacade = cct.creditFacade(); - creditConfigurator = cct.creditConfigurator(); - withdrawalManager = cct.withdrawalManager(); - - TARGET_CONTRACT = address(new TargetContractMock()); - - adapter1 = new AdapterMock(address(creditManager), TARGET_CONTRACT); - - adapterDifferentCM = new AdapterMock( - address(new CreditFacadeTestSuite(creditConfig, withDegenNFT, expirable, supportQuotas,1).creditManager()), TARGET_CONTRACT - ); - - DUMB_COMPARTIBLE_CONTRACT = address(adapter1); - } - - // - // HELPERS - // - function _compareParams( - uint16 feeInterest, - uint16 feeLiquidation, - uint16 liquidationDiscount, - uint16 feeLiquidationExpired, - uint16 liquidationDiscountExpired - ) internal { - ( - uint16 feeInterest2, - uint16 feeLiquidation2, - uint16 liquidationDiscount2, - uint16 feeLiquidationExpired2, - uint16 liquidationDiscountExpired2 - ) = creditManager.fees(); - - assertEq(feeInterest2, feeInterest, "Incorrect feeInterest"); - assertEq(feeLiquidation2, feeLiquidation, "Incorrect feeLiquidation"); - assertEq(liquidationDiscount2, liquidationDiscount, "Incorrect liquidationDiscount"); - assertEq(feeLiquidationExpired2, feeLiquidationExpired, "Incorrect feeLiquidationExpired"); - assertEq(liquidationDiscountExpired2, liquidationDiscountExpired, "Incorrect liquidationDiscountExpired"); - } - - function _getAddress(bytes memory bytecode, uint256 _salt) public view returns (address) { - bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), _salt, keccak256(bytecode))); - - // NOTE: cast last 20 bytes of hash to address - return address(uint160(uint256(hash))); - } - - function _deploy(bytes memory bytecode, uint256 _salt) public payable { - address addr; - - /* - NOTE: How to call create2 - create2(v, p, n, s) - create new contract with code at memory p to p + n - and send v wei - and return the new address - where new address = first 20 bytes of keccak256(0xff + address(this) + s + keccak256(mem[p…(p+n))) - s = big-endian 256-bit value - */ - assembly { - addr := - create2( - callvalue(), // wei sent with current call - // Actual code starts after skipping the first 32 bytes - add(bytecode, 0x20), - mload(bytecode), // Load the size of code contained in the first 32 bytes - _salt // Salt from function arguments - ) - - if iszero(extcodesize(addr)) { revert(0, 0) } - } - } - - /// - /// - /// TESTS - /// - /// - - /// @dev [CC-1]: constructor sets correct values - function test_CC_01_constructor_sets_correct_values() public { - assertEq(address(creditConfigurator.creditManager()), address(creditManager), "Incorrect creditManager"); - - assertEq(address(creditConfigurator.creditFacade()), address(creditFacade), "Incorrect creditFacade"); - - assertEq(address(creditConfigurator.underlying()), address(creditManager.underlying()), "Incorrect underlying"); - - assertEq( - address(creditConfigurator.addressProvider()), address(cct.addressProvider()), "Incorrect addressProvider" - ); - - // CREDIT MANAGER PARAMS - - ( - uint16 feeInterest, - uint16 feeLiquidation, - uint16 liquidationDiscount, - uint16 feeLiquidationExpired, - uint16 liquidationDiscountExpired - ) = creditManager.fees(); - - assertEq(feeInterest, DEFAULT_FEE_INTEREST, "Incorrect feeInterest"); - - assertEq(feeLiquidation, DEFAULT_FEE_LIQUIDATION, "Incorrect feeLiquidation"); - - assertEq(liquidationDiscount, PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM, "Incorrect liquidationDiscount"); - - assertEq(feeLiquidationExpired, DEFAULT_FEE_LIQUIDATION_EXPIRED, "Incorrect feeLiquidationExpired"); - - assertEq( - liquidationDiscountExpired, - PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM_EXPIRED, - "Incorrect liquidationDiscountExpired" - ); - - assertEq( - address(creditConfigurator.addressProvider()), address(cct.addressProvider()), "Incorrect address provider" - ); - - CollateralTokensItem[8] memory collateralTokenOpts = [ - CollateralTokensItem({token: Tokens.DAI, liquidationThreshold: DEFAULT_UNDERLYING_LT}), - CollateralTokensItem({token: Tokens.USDC, liquidationThreshold: 9000}), - CollateralTokensItem({token: Tokens.USDT, liquidationThreshold: 8800}), - CollateralTokensItem({token: Tokens.WETH, liquidationThreshold: 8300}), - CollateralTokensItem({token: Tokens.LINK, liquidationThreshold: 7300}), - CollateralTokensItem({token: Tokens.CRV, liquidationThreshold: 7300}), - CollateralTokensItem({token: Tokens.CVX, liquidationThreshold: 7300}), - CollateralTokensItem({token: Tokens.STETH, liquidationThreshold: 7300}) - ]; - - uint256 len = collateralTokenOpts.length; - - // Allowed Tokens - assertEq(creditManager.collateralTokensCount(), len, "Incorrect quantity of allowed tokens"); - - for (uint256 i = 0; i < len; i++) { - (address token, uint16 lt) = creditManager.collateralTokens(i); - - assertEq(token, tokenTestSuite.addressOf(collateralTokenOpts[i].token), "Incorrect token address"); - - assertEq(lt, collateralTokenOpts[i].liquidationThreshold, "Incorrect liquidation threshold"); - } - - assertEq(address(creditManager.creditFacade()), address(creditFacade), "Incorrect creditFacade"); - - assertEq(address(creditManager.priceOracle()), address(cct.priceOracle()), "Incorrect creditFacade"); - - // CREDIT FACADE PARAMS - (uint128 minBorrowedAmount, uint128 maxBorrowedAmount) = creditFacade.debtLimits(); - - assertEq(minBorrowedAmount, cct.minBorrowedAmount(), "Incorrect minBorrowedAmount"); - - assertEq(maxBorrowedAmount, cct.maxBorrowedAmount(), "Incorrect maxBorrowedAmount"); - - uint8 maxBorrowedAmountPerBlock = creditFacade.maxDebtPerBlockMultiplier(); - - uint40 expirationDate = creditFacade.expirationDate(); - - assertEq(maxBorrowedAmountPerBlock, DEFAULT_LIMIT_PER_BLOCK_MULTIPLIER, "Incorrect maxBorrowedAmountPerBlock"); - - assertEq(expirationDate, 0, "Incorrect expiration date"); - } - - /// @dev [CC-1A]: constructor emits all events - function test_CC_01A_constructor_emits_all_events() public { - CollateralToken[] memory cTokens = new CollateralToken[](1); - - cTokens[0] = CollateralToken({token: tokenTestSuite.addressOf(Tokens.USDC), liquidationThreshold: 6000}); - - CreditManagerOpts memory creditOpts = CreditManagerOpts({ - minBorrowedAmount: uint128(50 * WAD), - maxBorrowedAmount: uint128(150000 * WAD), - collateralTokens: cTokens, - degenNFT: address(0), - withdrawalManager: address(0), - expirable: false - }); - - creditManager = new CreditManagerV3(address(cct.poolMock()), address(withdrawalManager)); - creditFacade = new CreditFacadeV3( - address(creditManager), - creditOpts.degenNFT, - - creditOpts.expirable - ); - - address priceOracleAddress = address(creditManager.priceOracle()); - address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - - bytes memory configuratorByteCode = - abi.encodePacked(type(CreditConfigurator).creationCode, abi.encode(creditManager, creditFacade, creditOpts)); - - address creditConfiguratorAddr = _getAddress(configuratorByteCode, 0); - - creditManager.setCreditConfigurator(creditConfiguratorAddr); - - vm.expectEmit(true, false, false, true); - emit SetTokenLiquidationThreshold(underlying, DEFAULT_UNDERLYING_LT); - - vm.expectEmit(false, false, false, false); - emit FeesUpdated( - DEFAULT_FEE_INTEREST, - DEFAULT_FEE_LIQUIDATION, - DEFAULT_LIQUIDATION_PREMIUM, - DEFAULT_FEE_LIQUIDATION_EXPIRED, - DEFAULT_LIQUIDATION_PREMIUM_EXPIRED - ); - - vm.expectEmit(true, false, false, false); - emit AllowToken(usdcToken); - - vm.expectEmit(true, false, false, true); - emit SetTokenLiquidationThreshold(usdcToken, 6000); - - vm.expectEmit(true, false, false, false); - emit SetCreditFacade(address(creditFacade)); - - vm.expectEmit(true, false, false, false); - emit SetPriceOracle(priceOracleAddress); - - /// todo: change - // vm.expectEmit(false, false, false, true); - // emit SetMaxDebtPerBlockMultiplier(uint128(150000 * WAD * DEFAULT_LIMIT_PER_BLOCK_MULTIPLIER)); - - vm.expectEmit(false, false, false, true); - emit SetBorrowingLimits(uint128(50 * WAD), uint128(150000 * WAD)); - - _deploy(configuratorByteCode, 0); - } - - /// @dev [CC-2]: all functions revert if called non-configurator - function test_CC_02_all_functions_revert_if_called_non_configurator() public { - vm.startPrank(USER); - - // Token mgmt - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.addCollateralToken(DUMB_ADDRESS, 1); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.allowToken(DUMB_ADDRESS); - - // Contract mgmt - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.allowContract(DUMB_ADDRESS, DUMB_ADDRESS); - - // Credit manager mgmt - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.setFees(0, 0, 0, 0, 0); - - // Upgrades - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.setPriceOracle(); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.setCreditFacade(DUMB_ADDRESS, false); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.upgradeCreditConfigurator(DUMB_ADDRESS); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.setBotList(FRIEND); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.setMaxCumulativeLoss(0); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.resetCumulativeLoss(); - - vm.stopPrank(); - } - - function test_CC_02A_forbidBorrowing_on_non_pausable_admin() public { - vm.expectRevert(CallerNotPausableAdminException.selector); - creditConfigurator.forbidBorrowing(); - - vm.prank(CONFIGURATOR); - creditConfigurator.forbidBorrowing(); - } - - function test_CC_02B_controllerOnly_functions_revert_on_non_controller() public { - vm.expectRevert(CallerNotControllerException.selector); - creditConfigurator.setLiquidationThreshold(DUMB_ADDRESS, uint16(0)); - - vm.expectRevert(CallerNotPausableAdminException.selector); - creditConfigurator.forbidToken(DUMB_ADDRESS); - - vm.expectRevert(CallerNotControllerException.selector); - creditConfigurator.forbidContract(DUMB_ADDRESS); - - vm.expectRevert(CallerNotControllerException.selector); - creditConfigurator.setLimits(0, 0); - - vm.expectRevert(CallerNotControllerException.selector); - creditConfigurator.setMaxDebtPerBlockMultiplier(0); - - vm.expectRevert(CallerNotControllerException.selector); - creditConfigurator.setMaxEnabledTokens(1); - - vm.expectRevert(CallerNotControllerException.selector); - creditConfigurator.rampLiquidationThreshold(DUMB_ADDRESS, 0, 0, 0); - } - - // - // TOKEN MANAGEMENT - // - - /// @dev [CC-3]: addCollateralToken reverts for zero address or in priceFeed - function test_CC_03_addCollateralToken_reverts_for_zero_address_or_in_priceFeed() public { - vm.startPrank(CONFIGURATOR); - - vm.expectRevert(ZeroAddressException.selector); - creditConfigurator.addCollateralToken(address(0), 9300); - - vm.expectRevert(abi.encodeWithSelector(AddressIsNotContractException.selector, DUMB_ADDRESS)); - creditConfigurator.addCollateralToken(DUMB_ADDRESS, 9300); - - vm.expectRevert(IncorrectTokenContractException.selector); - creditConfigurator.addCollateralToken(address(this), 9300); - - address unknownPricefeedToken = address(new ERC20("TWPF", "Token without priceFeed")); - - vm.expectRevert(IncorrectPriceFeedException.selector); - creditConfigurator.addCollateralToken(unknownPricefeedToken, 9300); - - vm.stopPrank(); - } - - /// @dev [CC-4]: addCollateralToken adds new token to creditManager - function test_CC_04_addCollateralToken_adds_new_token_to_creditManager_and_set_lt() public { - uint256 tokensCountBefore = creditManager.collateralTokensCount(); - - address cLINKToken = tokenTestSuite.addressOf(Tokens.LUNA); - - vm.expectEmit(true, false, false, false); - emit AllowToken(cLINKToken); - - vm.prank(CONFIGURATOR); - creditConfigurator.addCollateralToken(cLINKToken, 8800); - - assertEq(creditManager.collateralTokensCount(), tokensCountBefore + 1, "Incorrect tokens count"); - - (address token,) = creditManager.collateralTokens(tokensCountBefore); - - assertEq(token, cLINKToken, "Token is not added to list"); - - assertTrue(creditManager.getTokenMaskOrRevert(cLINKToken) > 0, "Incorrect token mask"); - - assertEq(creditManager.liquidationThresholds(cLINKToken), 8800, "Threshold wasn't set"); - } - - /// @dev [CC-5]: setLiquidationThreshold reverts for underling token and incorrect values - function test_CC_05_setLiquidationThreshold_reverts_for_underling_token_and_incorrect_values() public { - vm.startPrank(CONFIGURATOR); - - vm.expectRevert(SetLTForUnderlyingException.selector); - creditConfigurator.setLiquidationThreshold(underlying, 1); - - address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - - uint16 maxAllowedLT = creditManager.liquidationThresholds(underlying); - vm.expectRevert(IncorrectLiquidationThresholdException.selector); - creditConfigurator.setLiquidationThreshold(usdcToken, maxAllowedLT + 1); - - vm.stopPrank(); - } - - /// @dev [CC-6]: setLiquidationThreshold sets liquidation threshold in creditManager - function test_CC_06_setLiquidationThreshold_sets_liquidation_threshold_in_creditManager() public { - address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - uint16 newLT = 24; - - vm.expectEmit(true, false, false, true); - emit SetTokenLiquidationThreshold(usdcToken, newLT); - - vm.prank(CONFIGURATOR); - creditConfigurator.setLiquidationThreshold(usdcToken, newLT); - - assertEq(creditManager.liquidationThresholds(usdcToken), newLT); - } - - /// @dev [CC-7]: allowToken and forbidToken reverts for unknown or underlying token - function test_CC_07_allowToken_and_forbidToken_reverts_for_unknown_or_underlying_token() public { - vm.startPrank(CONFIGURATOR); - - vm.expectRevert(TokenNotAllowedException.selector); - creditConfigurator.allowToken(DUMB_ADDRESS); - - vm.expectRevert(TokenNotAllowedException.selector); - creditConfigurator.allowToken(underlying); - - vm.expectRevert(TokenNotAllowedException.selector); - creditConfigurator.forbidToken(DUMB_ADDRESS); - - vm.expectRevert(TokenNotAllowedException.selector); - creditConfigurator.forbidToken(underlying); - - vm.stopPrank(); - } - - /// @dev [CC-8]: allowToken doesn't change forbidden mask if its already allowed - function test_CC_08_allowToken_doesnt_change_forbidden_mask_if_its_already_allowed() public { - address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - uint256 forbiddenMask = creditFacade.forbiddenTokenMask(); - - vm.prank(CONFIGURATOR); - creditConfigurator.allowToken(usdcToken); - - assertEq(creditFacade.forbiddenTokenMask(), forbiddenMask, "Incorrect forbidden mask"); - } - - // TODO: change tests - - // /// @dev [CC-9]: allowToken allows token if it was forbidden - // function test_CC_09_allows_token_if_it_was_forbidden() public { - // address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - // uint256 tokenMask = creditManager.getTokenMaskOrRevert(usdcToken); - - // vm.prank(address(creditConfigurator)); - // creditManager.setForbidMask(tokenMask); - - // vm.expectEmit(true, false, false, false); - // emit AllowToken(usdcToken); - - // vm.prank(CONFIGURATOR); - // creditConfigurator.allowToken(usdcToken); - - // assertEq(creditManager.forbiddenTokenMask(), 0, "Incorrect forbidden mask"); - // } - - // /// @dev [CC-10]: forbidToken doesn't change forbidden mask if its already forbidden - // function test_CC_10_forbidToken_doesnt_change_forbidden_mask_if_its_already_forbidden() public { - // address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - // uint256 tokenMask = creditManager.getTokenMaskOrRevert(usdcToken); - - // vm.prank(address(creditConfigurator)); - // creditManager.setForbidMask(tokenMask); - - // uint256 forbiddenMask = creditManager.forbiddenTokenMask(); - - // vm.prank(CONFIGURATOR); - // creditConfigurator.forbidToken(usdcToken); - - // assertEq(creditManager.forbiddenTokenMask(), forbiddenMask, "Incorrect forbidden mask"); - // } - - // /// @dev [CC-11]: forbidToken forbids token and enable IncreaseDebtForbidden mode if it was allowed - // function test_CC_11_forbidToken_forbids_token_if_it_was_allowed() public { - // address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - // uint256 tokenMask = creditManager.getTokenMaskOrRevert(usdcToken); - - // vm.prank(address(creditConfigurator)); - // creditManager.setForbidMask(0); - - // vm.expectEmit(true, false, false, false); - // emit ForbidToken(usdcToken); - - // vm.prank(CONFIGURATOR); - // creditConfigurator.forbidToken(usdcToken); - - // assertEq(creditManager.forbiddenTokenMask(), tokenMask, "Incorrect forbidden mask"); - // } - - // - // CONFIGURATION: CONTRACTS & ADAPTERS MANAGEMENT - // - - /// @dev [CC-12]: allowContract and forbidContract reverts for zero address - function test_CC_12_allowContract_and_forbidContract_reverts_for_zero_address() public { - vm.startPrank(CONFIGURATOR); - - vm.expectRevert(ZeroAddressException.selector); - creditConfigurator.allowContract(address(0), address(this)); - - vm.expectRevert(ZeroAddressException.selector); - creditConfigurator.allowContract(address(this), address(0)); - - vm.expectRevert(ZeroAddressException.selector); - creditConfigurator.forbidContract(address(0)); - - vm.stopPrank(); - } - - /// @dev [CC-12A]: allowContract reverts for non contract addresses - function test_CC_12A_allowContract_reverts_for_non_contract_addresses() public { - vm.startPrank(CONFIGURATOR); - - vm.expectRevert(abi.encodeWithSelector(AddressIsNotContractException.selector, DUMB_ADDRESS)); - creditConfigurator.allowContract(address(this), DUMB_ADDRESS); - - vm.expectRevert(abi.encodeWithSelector(AddressIsNotContractException.selector, DUMB_ADDRESS)); - creditConfigurator.allowContract(DUMB_ADDRESS, address(this)); - - vm.stopPrank(); - } - - /// @dev [CC-12B]: allowContract reverts for non compartible adapter contract - function test_CC_12B_allowContract_reverts_for_non_compartible_adapter_contract() public { - vm.startPrank(CONFIGURATOR); - - // Should be reverted, cause undelring token has no .creditManager() method - vm.expectRevert(IncompatibleContractException.selector); - creditConfigurator.allowContract(address(this), underlying); - - // Should be reverted, cause it's conncted to another creditManager - vm.expectRevert(IncompatibleContractException.selector); - creditConfigurator.allowContract(address(this), address(adapterDifferentCM)); - - vm.stopPrank(); - } - - /// @dev [CC-13]: allowContract reverts for creditManager and creditFacade contracts - function test_CC_13_allowContract_reverts_for_creditManager_and_creditFacade_contracts() public { - vm.startPrank(CONFIGURATOR); - - vm.expectRevert(TargetContractNotAllowedException.selector); - creditConfigurator.allowContract(address(creditManager), DUMB_COMPARTIBLE_CONTRACT); - - vm.expectRevert(TargetContractNotAllowedException.selector); - creditConfigurator.allowContract(DUMB_COMPARTIBLE_CONTRACT, address(creditFacade)); - - vm.expectRevert(TargetContractNotAllowedException.selector); - creditConfigurator.allowContract(address(creditFacade), DUMB_COMPARTIBLE_CONTRACT); - - vm.stopPrank(); - } - - /// @dev [CC-14]: allowContract: adapter could not be used twice - function test_CC_14_allowContract_adapter_cannot_be_used_twice() public { - vm.startPrank(CONFIGURATOR); - - creditConfigurator.allowContract(DUMB_COMPARTIBLE_CONTRACT, address(adapter1)); - - vm.expectRevert(AdapterUsedTwiceException.selector); - creditConfigurator.allowContract(address(adapterDifferentCM), address(adapter1)); - - vm.stopPrank(); - } - - /// @dev [CC-15]: allowContract allows targetContract <-> adapter and emits event - function test_CC_15_allowContract_allows_targetContract_adapter_and_emits_event() public { - address[] memory allowedContracts = creditConfigurator.allowedContracts(); - uint256 allowedContractCount = allowedContracts.length; - - vm.prank(CONFIGURATOR); - - vm.expectEmit(true, true, false, false); - emit AllowContract(TARGET_CONTRACT, address(adapter1)); - - assertTrue(!allowedContracts.includes(TARGET_CONTRACT), "Contract already added"); - - creditConfigurator.allowContract(TARGET_CONTRACT, address(adapter1)); - - assertEq( - creditManager.adapterToContract(address(adapter1)), TARGET_CONTRACT, "adapterToContract wasn't udpated" - ); - - assertEq( - creditManager.contractToAdapter(TARGET_CONTRACT), address(adapter1), "contractToAdapter wasn't udpated" - ); - - allowedContracts = creditConfigurator.allowedContracts(); - - assertEq(allowedContracts.length, allowedContractCount + 1, "Incorrect allowed contracts count"); - - assertTrue(allowedContracts.includes(TARGET_CONTRACT), "Target contract wasnt found"); - } - - // /// @dev [CC-15A]: allowContract allows universal adapter for universal contract - // function test_CC_15A_allowContract_allows_universal_contract() public { - // vm.prank(CONFIGURATOR); - - // vm.expectEmit(true, true, false, false); - // emit AllowContract(UNIVERSAL_CONTRACT, address(adapter1)); - - // creditConfigurator.allowContract(UNIVERSAL_CONTRACT, address(adapter1)); - - // assertEq(creditManager.universalAdapter(), address(adapter1), "Universal adapter wasn't updated"); - // } - - /// @dev [CC-15A]: allowContract removes existing adapter - function test_CC_15A_allowContract_removes_old_adapter_if_it_exists() public { - vm.prank(CONFIGURATOR); - creditConfigurator.allowContract(TARGET_CONTRACT, address(adapter1)); - - AdapterMock adapter2 = new AdapterMock( - address(creditManager), - TARGET_CONTRACT - ); - - vm.prank(CONFIGURATOR); - creditConfigurator.allowContract(TARGET_CONTRACT, address(adapter2)); - - assertEq(creditManager.contractToAdapter(TARGET_CONTRACT), address(adapter2), "Incorrect adapter"); - - assertEq( - creditManager.adapterToContract(address(adapter2)), - TARGET_CONTRACT, - "Incorrect target contract for new adapter" - ); - - assertEq(creditManager.adapterToContract(address(adapter1)), address(0), "Old adapter was not removed"); - } - - /// @dev [CC-16]: forbidContract reverts for unknown contract - function test_CC_16_forbidContract_reverts_for_unknown_contract() public { - vm.expectRevert(ContractIsNotAnAllowedAdapterException.selector); - - vm.prank(CONFIGURATOR); - creditConfigurator.forbidContract(TARGET_CONTRACT); - } - - /// @dev [CC-17]: forbidContract forbids contract and emits event - function test_CC_17_forbidContract_forbids_contract_and_emits_event() public { - vm.startPrank(CONFIGURATOR); - creditConfigurator.allowContract(DUMB_COMPARTIBLE_CONTRACT, address(adapter1)); - - address[] memory allowedContracts = creditConfigurator.allowedContracts(); - - uint256 allowedContractCount = allowedContracts.length; - - assertTrue(allowedContracts.includes(DUMB_COMPARTIBLE_CONTRACT), "Target contract wasnt found"); - - vm.expectEmit(true, false, false, false); - emit ForbidContract(DUMB_COMPARTIBLE_CONTRACT); - - creditConfigurator.forbidContract(DUMB_COMPARTIBLE_CONTRACT); - - // - allowedContracts = creditConfigurator.allowedContracts(); - - assertEq(creditManager.adapterToContract(address(adapter1)), address(0), "CreditManagerV3 wasn't udpated"); - - assertEq( - creditManager.contractToAdapter(DUMB_COMPARTIBLE_CONTRACT), address(0), "CreditFacadeV3 wasn't udpated" - ); - - assertEq(allowedContracts.length, allowedContractCount - 1, "Incorrect allowed contracts count"); - - assertTrue(!allowedContracts.includes(DUMB_COMPARTIBLE_CONTRACT), "Target contract wasn't removed"); - - vm.stopPrank(); - } - - // - // CREDIT MANAGER MGMT - // - - /// @dev [CC-18]: setLimits reverts if minAmount > maxAmount - function test_CC_18_setLimits_reverts_if_minAmount_gt_maxAmount() public { - (uint128 minBorrowedAmount, uint128 maxBorrowedAmount) = creditFacade.debtLimits(); - - vm.expectRevert(IncorrectLimitsException.selector); - - vm.prank(CONFIGURATOR); - creditConfigurator.setLimits(maxBorrowedAmount, minBorrowedAmount); - } - - /// @dev [CC-19]: setLimits sets limits - function test_CC_19_setLimits_sets_limits() public { - (uint128 minBorrowedAmount, uint128 maxBorrowedAmount) = creditFacade.debtLimits(); - uint128 newMinBorrowedAmount = minBorrowedAmount + 1000; - uint128 newMaxBorrowedAmount = maxBorrowedAmount + 1000; - - vm.expectEmit(false, false, false, true); - emit SetBorrowingLimits(newMinBorrowedAmount, newMaxBorrowedAmount); - vm.prank(CONFIGURATOR); - creditConfigurator.setLimits(newMinBorrowedAmount, newMaxBorrowedAmount); - (minBorrowedAmount, maxBorrowedAmount) = creditFacade.debtLimits(); - assertEq(minBorrowedAmount, newMinBorrowedAmount, "Incorrect minBorrowedAmount"); - assertEq(maxBorrowedAmount, newMaxBorrowedAmount, "Incorrect maxBorrowedAmount"); - } - - /// @dev [CC-23]: setFees reverts for incorrect fees - function test_CC_23_setFees_reverts_for_incorrect_fees() public { - (, uint16 feeLiquidation,, uint16 feeLiquidationExpired,) = creditManager.fees(); - - vm.expectRevert(IncorrectParameterException.selector); - - vm.prank(CONFIGURATOR); - creditConfigurator.setFees(PERCENTAGE_FACTOR, feeLiquidation, 0, 0, 0); - - vm.expectRevert(IncorrectParameterException.selector); - - vm.prank(CONFIGURATOR); - creditConfigurator.setFees(PERCENTAGE_FACTOR - 1, feeLiquidation, PERCENTAGE_FACTOR - feeLiquidation, 0, 0); - - vm.expectRevert(IncorrectParameterException.selector); - - vm.prank(CONFIGURATOR); - creditConfigurator.setFees( - PERCENTAGE_FACTOR - 1, - feeLiquidation, - PERCENTAGE_FACTOR - feeLiquidation - 1, - feeLiquidationExpired, - PERCENTAGE_FACTOR - feeLiquidationExpired - ); - } - - /// @dev [CC-25]: setFees updates LT for underlying and for all tokens which bigger than new LT - function test_CC_25_setFees_updates_LT_for_underlying_and_for_all_tokens_which_bigger_than_new_LT() public { - vm.startPrank(CONFIGURATOR); - - (uint16 feeInterest,,,,) = creditManager.fees(); - - address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - address wethToken = tokenTestSuite.addressOf(Tokens.WETH); - creditConfigurator.setLiquidationThreshold(usdcToken, creditManager.liquidationThresholds(underlying)); - - uint256 expectedLT = PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM - 2 * DEFAULT_FEE_LIQUIDATION; - - uint256 wethLTBefore = creditManager.liquidationThresholds(wethToken); - - vm.expectEmit(true, false, false, true); - emit SetTokenLiquidationThreshold(usdcToken, uint16(expectedLT)); - - vm.expectEmit(true, false, false, true); - emit SetTokenLiquidationThreshold(underlying, uint16(expectedLT)); - - creditConfigurator.setFees( - feeInterest, - 2 * DEFAULT_FEE_LIQUIDATION, - DEFAULT_LIQUIDATION_PREMIUM, - DEFAULT_FEE_LIQUIDATION_EXPIRED, - DEFAULT_LIQUIDATION_PREMIUM_EXPIRED - ); - - assertEq(creditManager.liquidationThresholds(underlying), expectedLT, "Incorrect LT for underlying token"); - - assertEq(creditManager.liquidationThresholds(usdcToken), expectedLT, "Incorrect USDC for underlying token"); - - assertEq(creditManager.liquidationThresholds(wethToken), wethLTBefore, "Incorrect WETH for underlying token"); - } - - /// @dev [CC-26]: setFees sets fees and doesn't change others - function test_CC_26_setFees_sets_fees_and_doesnt_change_others() public { - ( - uint16 feeInterest, - uint16 feeLiquidation, - uint16 liquidationDiscount, - uint16 feeLiquidationExpired, - uint16 liquidationDiscountExpired - ) = creditManager.fees(); - - uint16 newFeeInterest = (feeInterest * 3) / 2; - uint16 newFeeLiquidation = feeLiquidation * 2; - uint16 newLiquidationPremium = (PERCENTAGE_FACTOR - liquidationDiscount) * 2; - uint16 newFeeLiquidationExpired = feeLiquidationExpired * 2; - uint16 newLiquidationPremiumExpired = (PERCENTAGE_FACTOR - liquidationDiscountExpired) * 2; - - vm.expectEmit(false, false, false, true); - emit FeesUpdated( - newFeeInterest, - newFeeLiquidation, - newLiquidationPremium, - newFeeLiquidationExpired, - newLiquidationPremiumExpired - ); - - vm.prank(CONFIGURATOR); - creditConfigurator.setFees( - newFeeInterest, - newFeeLiquidation, - newLiquidationPremium, - newFeeLiquidationExpired, - newLiquidationPremiumExpired - ); - - _compareParams( - newFeeInterest, - newFeeLiquidation, - PERCENTAGE_FACTOR - newLiquidationPremium, - newFeeLiquidationExpired, - PERCENTAGE_FACTOR - newLiquidationPremiumExpired - ); - } - - // - // CONTRACT UPGRADES - // - - /// @dev [CC-28]: setPriceOracle upgrades priceOracleCorrectly and doesnt change facade - function test_CC_28_setPriceOracle_upgrades_priceOracleCorrectly_and_doesnt_change_facade() public { - vm.startPrank(CONFIGURATOR); - cct.addressProvider().setPriceOracle(DUMB_ADDRESS); - - vm.expectEmit(true, false, false, false); - emit SetPriceOracle(DUMB_ADDRESS); - - creditConfigurator.setPriceOracle(); - - assertEq(address(creditManager.priceOracle()), DUMB_ADDRESS); - vm.stopPrank(); - } - - /// @dev [CC-29]: setPriceOracle upgrades priceOracleCorrectly and doesnt change facade - function test_CC_29_setCreditFacade_upgradeCreditConfigurator_reverts_for_incompatible_contracts() public { - vm.startPrank(CONFIGURATOR); - - vm.expectRevert(ZeroAddressException.selector); - creditConfigurator.setCreditFacade(address(0), false); - - vm.expectRevert(ZeroAddressException.selector); - creditConfigurator.upgradeCreditConfigurator(address(0)); - - vm.expectRevert(abi.encodeWithSelector(AddressIsNotContractException.selector, DUMB_ADDRESS)); - creditConfigurator.setCreditFacade(DUMB_ADDRESS, false); - - vm.expectRevert(abi.encodeWithSelector(AddressIsNotContractException.selector, DUMB_ADDRESS)); - creditConfigurator.upgradeCreditConfigurator(DUMB_ADDRESS); - - vm.expectRevert(IncompatibleContractException.selector); - creditConfigurator.setCreditFacade(underlying, false); - - vm.expectRevert(IncompatibleContractException.selector); - creditConfigurator.upgradeCreditConfigurator(underlying); - - vm.expectRevert(IncompatibleContractException.selector); - creditConfigurator.setCreditFacade(address(adapterDifferentCM), false); - - vm.expectRevert(IncompatibleContractException.selector); - creditConfigurator.upgradeCreditConfigurator(address(adapterDifferentCM)); - } - - /// @dev [CC-30]: setCreditFacade upgrades creditFacade and doesnt change priceOracle - function test_CC_30_setCreditFacade_upgrades_creditFacade_and_doesnt_change_priceOracle() public { - for (uint256 ex = 0; ex < 2; ex++) { - bool isExpirable = ex != 0; - for (uint256 ms = 0; ms < 2; ms++) { - bool migrateSettings = ms != 0; - - setUp(); - - if (isExpirable) { - CreditFacadeV3 initialCf = new CreditFacadeV3( - address(creditManager), - address(0), - - true - ); - - vm.prank(CONFIGURATOR); - creditConfigurator.setCreditFacade(address(initialCf), migrateSettings); - - vm.prank(CONFIGURATOR); - creditConfigurator.setExpirationDate(uint40(block.timestamp + 1)); - - creditFacade = initialCf; - } - - CreditFacadeV3 cf = new CreditFacadeV3( - address(creditManager), - address(0), - isExpirable - ); - - uint8 maxDebtPerBlockMultiplier = creditFacade.maxDebtPerBlockMultiplier(); - - uint40 expirationDate = creditFacade.expirationDate(); - (uint128 minBorrowedAmount, uint128 maxBorrowedAmount) = creditFacade.debtLimits(); - - vm.expectEmit(true, false, false, false); - emit SetCreditFacade(address(cf)); - - vm.prank(CONFIGURATOR); - creditConfigurator.setCreditFacade(address(cf), migrateSettings); - - assertEq(address(creditManager.priceOracle()), cct.addressProvider().getPriceOracle()); - - assertEq(address(creditManager.creditFacade()), address(cf)); - assertEq(address(creditConfigurator.creditFacade()), address(cf)); - - uint8 maxDebtPerBlockMultiplier2 = cf.maxDebtPerBlockMultiplier(); - - uint40 expirationDate2 = cf.expirationDate(); - - (uint128 minBorrowedAmount2, uint128 maxBorrowedAmount2) = cf.debtLimits(); - - assertEq( - maxDebtPerBlockMultiplier2, - migrateSettings ? maxDebtPerBlockMultiplier : 0, - "Incorrwect limitPerBlock" - ); - assertEq(minBorrowedAmount2, migrateSettings ? minBorrowedAmount : 0, "Incorrwect minBorrowedAmount"); - assertEq(maxBorrowedAmount2, migrateSettings ? maxBorrowedAmount : 0, "Incorrwect maxBorrowedAmount"); - - assertEq(expirationDate2, migrateSettings ? expirationDate : 0, "Incorrect expirationDate"); - } - } - } - - /// @dev [CC-30A]: usetCreditFacade transfers bot list - function test_CC_30A_botList_is_transferred_on_CreditFacade_upgrade() public { - for (uint256 ms = 0; ms < 2; ms++) { - bool migrateSettings = ms != 0; - - setUp(); - - address botList = address(new BotList(address(cct.addressProvider()))); - - vm.prank(CONFIGURATOR); - creditConfigurator.setBotList(botList); - - CreditFacadeV3 cf = new CreditFacadeV3( - address(creditManager), - address(0), - false - ); - - vm.prank(CONFIGURATOR); - creditConfigurator.setCreditFacade(address(cf), migrateSettings); - - address botList2 = cf.botList(); - - assertEq(botList2, migrateSettings ? botList : address(0), "Bot list was not transferred"); - } - } - - /// @dev [CC-31]: uupgradeCreditConfigurator upgrades creditConfigurator - function test_CC_31_upgradeCreditConfigurator_upgrades_creditConfigurator() public { - vm.expectEmit(true, false, false, false); - emit CreditConfiguratorUpgraded(DUMB_COMPARTIBLE_CONTRACT); - - vm.prank(CONFIGURATOR); - creditConfigurator.upgradeCreditConfigurator(DUMB_COMPARTIBLE_CONTRACT); - - assertEq(address(creditManager.creditConfigurator()), DUMB_COMPARTIBLE_CONTRACT); - } - - /// @dev [CC-32]: setBorrowingAllowance sets IncreaseDebtForbidden - function test_CC_32_setBorrowingAllowance_sets_IncreaseDebtForbidden() public { - /// TODO: Change test - // for (uint256 id = 0; id < 2; id++) { - // bool isIDF = id != 0; - // for (uint256 ii = 0; ii < 2; ii++) { - // bool initialIDF = ii != 0; - - // setUp(); - - // vm.prank(CONFIGURATOR); - // creditConfigurator.setBorrowingAllowance(initialIDF); - - // (, bool isIncreaseDebtFobidden,) = creditFacade.params(); - - // if (isIncreaseDebtFobidden != isIDF) { - // vm.expectEmit(false, false, false, true); - // emit SetIncreaseDebtForbiddenMode(isIDF); - // } - - // vm.prank(CONFIGURATOR); - // creditConfigurator.setBorrowingAllowance(isIDF); - - // (, isIncreaseDebtFobidden,) = creditFacade.params(); - - // assertTrue(isIncreaseDebtFobidden == isIDF, "Incorrect isIncreaseDebtFobidden"); - // } - // } - } - - /// @dev [CC-33]: setMaxDebtLimitPerBlock reverts if it lt maxLimit otherwise sets limitPerBlock - function test_CC_33_setMaxDebtLimitPerBlock_reverts_if_it_lt_maxLimit_otherwise_sets_limitPerBlock() public { - // (, uint128 maxBorrowedAmount) = creditFacade.debtLimits(); - - // vm.prank(CONFIGURATOR); - // vm.expectRevert(IncorrectLimitsException.selector); - // creditConfigurator.setMaxDebtLimitPerBlock(maxBorrowedAmount - 1); - - // uint128 newLimitBlock = (maxBorrowedAmount * 12) / 10; - - // vm.expectEmit(false, false, false, true); - // emit SetMaxDebtPerBlockMultiplier(newLimitBlock); - - // vm.prank(CONFIGURATOR); - // creditConfigurator.setMaxDebtLimitPerBlock(newLimitBlock); - - // (uint128 maxBorrowedAmountPerBlock,,) = creditFacade.params(); - - // assertEq(maxBorrowedAmountPerBlock, newLimitBlock, "Incorrect new limits block"); - } - - /// @dev [CC-34]: setExpirationDate reverts if the new expiration date is stale, otherwise sets it - function test_CC_34_setExpirationDate_reverts_on_incorrect_newExpirationDate_otherwise_sets() public { - // cct.testFacadeWithExpiration(); - // creditFacade = cct.creditFacade(); - - _setUp({withDegenNFT: false, expirable: true, supportQuotas: true}); - - uint40 expirationDate = creditFacade.expirationDate(); - - vm.prank(CONFIGURATOR); - vm.expectRevert(IncorrectExpirationDateException.selector); - creditConfigurator.setExpirationDate(expirationDate); - - vm.warp(block.timestamp + 10); - - vm.prank(CONFIGURATOR); - vm.expectRevert(IncorrectExpirationDateException.selector); - creditConfigurator.setExpirationDate(expirationDate + 1); - - uint40 newExpirationDate = uint40(block.timestamp + 1); - - vm.expectEmit(false, false, false, true); - emit SetExpirationDate(newExpirationDate); - - vm.prank(CONFIGURATOR); - creditConfigurator.setExpirationDate(newExpirationDate); - - expirationDate = creditFacade.expirationDate(); - - assertEq(expirationDate, newExpirationDate, "Incorrect new expirationDate"); - } - - /// @dev [CC-37]: setMaxEnabledTokens works correctly and emits event - function test_CC_37_setMaxEnabledTokens_works_correctly() public { - vm.expectRevert(CallerNotControllerException.selector); - creditConfigurator.setMaxEnabledTokens(255); - - vm.expectEmit(false, false, false, true); - emit SetMaxEnabledTokens(255); - - vm.prank(CONFIGURATOR); - creditConfigurator.setMaxEnabledTokens(255); - - assertEq(creditManager.maxAllowedEnabledTokenLength(), 255, "Credit manager max enabled tokens incorrect"); - } - - /// @dev [CC-38]: addEmergencyLiquidator works correctly and emits event - function test_CC_38_addEmergencyLiquidator_works_correctly() public { - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.addEmergencyLiquidator(DUMB_ADDRESS); - - vm.expectEmit(false, false, false, true); - emit AddEmergencyLiquidator(DUMB_ADDRESS); - - vm.prank(CONFIGURATOR); - creditConfigurator.addEmergencyLiquidator(DUMB_ADDRESS); - - assertTrue( - creditFacade.canLiquidateWhilePaused(DUMB_ADDRESS), "Credit manager emergency liquidator status incorrect" - ); - } - - /// @dev [CC-39]: removeEmergencyLiquidator works correctly and emits event - function test_CC_39_removeEmergencyLiquidator_works_correctly() public { - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.removeEmergencyLiquidator(DUMB_ADDRESS); - - vm.prank(CONFIGURATOR); - creditConfigurator.addEmergencyLiquidator(DUMB_ADDRESS); - - vm.expectEmit(false, false, false, true); - emit RemoveEmergencyLiquidator(DUMB_ADDRESS); - - vm.prank(CONFIGURATOR); - creditConfigurator.removeEmergencyLiquidator(DUMB_ADDRESS); - - assertTrue( - !creditFacade.canLiquidateWhilePaused(DUMB_ADDRESS), "Credit manager emergency liquidator status incorrect" - ); - } - - /// @dev [CC-40]: forbidAdapter works correctly and emits event - function test_CC_40_forbidAdapter_works_correctly() public { - vm.expectRevert(CallerNotConfiguratorException.selector); - creditConfigurator.forbidAdapter(DUMB_ADDRESS); - - vm.prank(CONFIGURATOR); - creditConfigurator.allowContract(TARGET_CONTRACT, address(adapter1)); - - vm.expectEmit(true, false, false, false); - emit ForbidAdapter(address(adapter1)); - - vm.prank(CONFIGURATOR); - creditConfigurator.forbidAdapter(address(adapter1)); - - assertEq( - creditManager.adapterToContract(address(adapter1)), address(0), "Adapter to contract link was not removed" - ); - - assertEq( - creditManager.contractToAdapter(TARGET_CONTRACT), address(adapter1), "Contract to adapter link was removed" - ); - } - - /// @dev [CC-41]: allowedContracts migrate correctly - function test_CC_41_allowedContracts_are_migrated_correctly_for_new_CC() public { - vm.prank(CONFIGURATOR); - creditConfigurator.allowContract(TARGET_CONTRACT, address(adapter1)); - - CollateralToken[] memory cTokens; - - CreditManagerOpts memory creditOpts = CreditManagerOpts({ - minBorrowedAmount: uint128(50 * WAD), - maxBorrowedAmount: uint128(150000 * WAD), - collateralTokens: cTokens, - degenNFT: address(0), - withdrawalManager: address(0), - expirable: false - }); - - CreditConfigurator newCC = new CreditConfigurator( - creditManager, - creditFacade, - creditOpts - ); - - assertEq( - creditConfigurator.allowedContracts().length, - newCC.allowedContracts().length, - "Incorrect new allowed contracts array" - ); - - uint256 len = newCC.allowedContracts().length; - - for (uint256 i = 0; i < len;) { - assertEq( - creditConfigurator.allowedContracts()[i], - newCC.allowedContracts()[i], - "Allowed contracts migrated incorrectly" - ); - - unchecked { - ++i; - } - } - } - - function test_CC_42_rampLiquidationThreshold_works_correctly() public { - address dai = tokenTestSuite.addressOf(Tokens.DAI); - address usdc = tokenTestSuite.addressOf(Tokens.USDC); - - vm.expectRevert(SetLTForUnderlyingException.selector); - vm.prank(CONFIGURATOR); - creditConfigurator.rampLiquidationThreshold(dai, 9000, uint40(block.timestamp), 1); - - vm.expectRevert(IncorrectLiquidationThresholdException.selector); - vm.prank(CONFIGURATOR); - creditConfigurator.rampLiquidationThreshold(usdc, 9999, uint40(block.timestamp), 1); - - uint16 initialLT = creditManager.liquidationThresholds(usdc); - - // vm.expectCall( - // address(creditManager), - // abi.encodeCall(CreditManagerV3.rampLiquidationThreshold, (usdc, 8900, uint40(block.timestamp + 5), 1000)) - // ); - - vm.expectEmit(true, false, false, true); - emit ScheduleTokenLiquidationThresholdRamp( - usdc, initialLT, 8900, uint40(block.timestamp + 5), uint40(block.timestamp + 1005) - ); - - vm.prank(CONFIGURATOR); - creditConfigurator.rampLiquidationThreshold(usdc, 8900, uint40(block.timestamp + 5), 1000); - } -} diff --git a/contracts/test/integration/credit/CreditFacade.t.sol b/contracts/test/integration/credit/CreditFacade.int.t.sol similarity index 82% rename from contracts/test/integration/credit/CreditFacade.t.sol rename to contracts/test/integration/credit/CreditFacade.int.t.sol index 05d2de0f..8358a5ea 100644 --- a/contracts/test/integration/credit/CreditFacade.t.sol +++ b/contracts/test/integration/credit/CreditFacade.int.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; @@ -19,7 +19,8 @@ import { ICreditManagerV3, ICreditManagerV3Events, ClosureAction, - ManageDebtAction + ManageDebtAction, + BOT_PERMISSIONS_SET_FLAG } from "../../../interfaces/ICreditManagerV3.sol"; import {AllowanceAction} from "../../../interfaces/ICreditConfiguratorV3.sol"; import {ICreditFacadeEvents} from "../../../interfaces/ICreditFacade.sol"; @@ -47,9 +48,10 @@ import {CreditFacadeTestHelper} from "../../helpers/CreditFacadeTestHelper.sol"; import "../../../interfaces/IExceptions.sol"; // MOCKS -import {AdapterMock} from "../../mocks/adapters/AdapterMock.sol"; +import {AdapterMock} from "../../mocks//adapters/AdapterMock.sol"; import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/adapters/TargetContractMock.sol"; -import {ERC20BlacklistableMock} from "../../mocks/token/ERC20Blacklistable.sol"; +import {ERC20BlacklistableMock} from "../../mocks//token/ERC20Blacklistable.sol"; +import {GeneralMock} from "../../mocks//GeneralMock.sol"; // SUITES import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; @@ -79,6 +81,8 @@ contract CreditFacadeIntegrationTest is TargetContractMock targetMock; AdapterMock adapterMock; + BotList botList; + function setUp() public { _setUp(Tokens.DAI); } @@ -179,18 +183,18 @@ contract CreditFacadeIntegrationTest is // TODO: ideas how to revert with ZA? - // /// @dev [FA-1]: constructor reverts for zero address - // function test_FA_01_constructor_reverts_for_zero_address() public { + // /// @dev I:[FA-1]: constructor reverts for zero address + // function test_I_FA_01_constructor_reverts_for_zero_address() public { // vm.expectRevert(ZeroAddressException.selector); // new CreditFacadeV3(address(0), address(0), address(0), false); // } - /// @dev [FA-1A]: constructor sets correct values - function test_FA_01A_constructor_sets_correct_values() public { + /// @dev I:[FA-1A]: constructor sets correct values + function test_I_FA_01A_constructor_sets_correct_values() public { assertEq(address(creditFacade.creditManager()), address(creditManager), "Incorrect creditManager"); - assertEq(creditFacade.underlying(), underlying, "Incorrect underlying token"); + // assertEq(creditFacade.underlying(), underlying, "Incorrect underlying token"); - assertEq(creditFacade.wethAddress(), creditManager.wethAddress(), "Incorrect wethAddress token"); + assertEq(creditFacade.weth(), creditManager.weth(), "Incorrect weth token"); assertEq(creditFacade.degenNFT(), address(0), "Incorrect degenNFT"); @@ -214,8 +218,8 @@ contract CreditFacadeIntegrationTest is // ALL FUNCTIONS REVERTS IF USER HAS NO ACCOUNT // - /// @dev [FA-2]: functions reverts if borrower has no account - function test_FA_02_functions_reverts_if_borrower_has_no_account() public { + /// @dev I:[FA-2]: functions reverts if borrower has no account + function test_I_FA_02_functions_reverts_if_borrower_has_no_account() public { vm.expectRevert(CreditAccountNotExistsException.selector); vm.prank(USER); creditFacade.closeCreditAccount(DUMB_ADDRESS, FRIEND, 0, false, multicallBuilder()); @@ -262,7 +266,7 @@ contract CreditFacadeIntegrationTest is // // ETH => WETH TESTS // - function test_FA_03B_openCreditAccountMulticall_correctly_wraps_ETH() public { + function test_I_FA_03B_openCreditAccountMulticall_correctly_wraps_ETH() public { /// - openCreditAccount _prepareForWETHTest(); @@ -277,13 +281,12 @@ contract CreditFacadeIntegrationTest is callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) }) ), - false, 0 ); _checkForWETHTest(); } - function test_FA_03C_closeCreditAccount_correctly_wraps_ETH() public { + function test_I_FA_03C_closeCreditAccount_correctly_wraps_ETH() public { (address creditAccount,) = _openTestCreditAccount(); vm.roll(block.number + 1); @@ -294,7 +297,7 @@ contract CreditFacadeIntegrationTest is _checkForWETHTest(); } - function test_FA_03D_liquidate_correctly_wraps_ETH() public { + function test_I_FA_03D_liquidate_correctly_wraps_ETH() public { (address creditAccount,) = _openTestCreditAccount(); vm.roll(block.number + 1); @@ -314,7 +317,7 @@ contract CreditFacadeIntegrationTest is _checkForWETHTest(LIQUIDATOR); } - function test_FA_03F_multicall_correctly_wraps_ETH() public { + function test_I_FA_03F_multicall_correctly_wraps_ETH() public { (address creditAccount,) = _openTestCreditAccount(); // MULTICALL @@ -337,8 +340,8 @@ contract CreditFacadeIntegrationTest is // OPEN CREDIT ACCOUNT // - /// @dev [FA-4A]: openCreditAccount reverts for using addresses which is not allowed by transfer allowance - function test_FA_04A_openCreditAccount_reverts_for_using_addresses_which_is_not_allowed_by_transfer_allowance() + /// @dev I:[FA-4A]: openCreditAccount reverts for using addresses which is not allowed by transfer allowance + function test_I_FA_04A_openCreditAccount_reverts_for_using_addresses_which_is_not_allowed_by_transfer_allowance() public { (uint256 minBorrowedAmount,) = creditFacade.debtLimits(); @@ -352,13 +355,13 @@ contract CreditFacadeIntegrationTest is }) ); vm.expectRevert(AccountTransferNotAllowedException.selector); - creditFacade.openCreditAccount(minBorrowedAmount, FRIEND, calls, false, 0); + creditFacade.openCreditAccount(minBorrowedAmount, FRIEND, calls, 0); vm.stopPrank(); } - /// @dev [FA-4B]: openCreditAccount reverts if user has no NFT for degen mode - function test_FA_04B_openCreditAccount_reverts_for_non_whitelisted_account() public { + /// @dev I:[FA-4B]: openCreditAccount reverts if user has no NFT for degen mode + function test_I_FA_04B_openCreditAccount_reverts_for_non_whitelisted_account() public { _setUp({ _underlying: Tokens.DAI, withDegenNFT: true, @@ -381,13 +384,12 @@ contract CreditFacadeIntegrationTest is callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) }) ), - false, 0 ); } - /// @dev [FA-4C]: openCreditAccount opens account and burns token - function test_FA_04C_openCreditAccount_burns_token_in_whitelisted_mode() public { + /// @dev I:[FA-4C]: openCreditAccount opens account and burns token + function test_I_FA_04C_openCreditAccount_burns_token_in_whitelisted_mode() public { _setUp({ _underlying: Tokens.DAI, withDegenNFT: true, @@ -421,15 +423,14 @@ contract CreditFacadeIntegrationTest is callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT)) }) ), - false, 0 ); expectBalance(address(degenNFT), USER, 0); } - // /// @dev [FA-5]: openCreditAccount sets correct values - // function test_FA_05_openCreditAccount_sets_correct_values() public { + // /// @dev I:[FA-5]: openCreditAccount sets correct values + // function test_I_FA_05_openCreditAccount_sets_correct_values() public { // uint16 LEVERAGE = 300; // x3 // address expectedCreditAccountAddress = accountFactory.head(); @@ -467,8 +468,8 @@ contract CreditFacadeIntegrationTest is // creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, FRIEND, LEVERAGE, REFERRAL_CODE); // } - /// @dev [FA-7]: openCreditAccount and openCreditAccount reverts when debt increase is forbidden - function test_FA_07_openCreditAccountMulticall_reverts_if_borrowing_forbidden() public { + /// @dev I:[FA-7]: openCreditAccount and openCreditAccount reverts when debt increase is forbidden + function test_I_FA_07_openCreditAccountMulticall_reverts_if_borrowing_forbidden() public { (uint256 minBorrowedAmount,) = creditFacade.debtLimits(); vm.prank(CONFIGURATOR); @@ -483,11 +484,11 @@ contract CreditFacadeIntegrationTest is vm.expectRevert(BorrowedBlockLimitException.selector); vm.prank(USER); - creditFacade.openCreditAccount(minBorrowedAmount, USER, calls, false, 0); + creditFacade.openCreditAccount(minBorrowedAmount, USER, calls, 0); } - /// @dev [FA-8]: openCreditAccount runs operations in correct order - function test_FA_08_openCreditAccountMulticall_runs_operations_in_correct_order() public { + /// @dev I:[FA-8]: openCreditAccount runs operations in correct order + function test_I_FA_08_openCreditAccountMulticall_runs_operations_in_correct_order() public { vm.prank(FRIEND); creditFacade.approveAccountTransfer(USER, true); @@ -514,8 +515,7 @@ contract CreditFacadeIntegrationTest is // EXPECTED STACK TRACE & EVENTS vm.expectCall( - address(creditManager), - abi.encodeCall(ICreditManagerV3.openCreditAccount, (DAI_ACCOUNT_AMOUNT, FRIEND, false)) + address(creditManager), abi.encodeCall(ICreditManagerV3.openCreditAccount, (DAI_ACCOUNT_AMOUNT, FRIEND)) ); vm.expectEmit(true, true, false, true); @@ -551,11 +551,11 @@ contract CreditFacadeIntegrationTest is ); vm.prank(USER); - creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, FRIEND, calls, false, REFERRAL_CODE); + creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, FRIEND, calls, REFERRAL_CODE); } - /// @dev [FA-9]: openCreditAccount cant open credit account with hf <1; - function test_FA_09_openCreditAccountMulticall_cant_open_credit_account_with_hf_less_one( + /// @dev I:[FA-9]: openCreditAccount cant open credit account with hf <1; + function test_I_FA_09_openCreditAccountMulticall_cant_open_credit_account_with_hf_less_one( uint256 amount, uint8 token1 ) public { @@ -570,7 +570,7 @@ contract CreditFacadeIntegrationTest is vm.prank(CONFIGURATOR); creditConfigurator.setLimits(1, type(uint96).max); - (address collateral,) = creditManager.collateralTokens(token1); + (address collateral,) = creditManager.collateralTokensByMask(1 << token1); tokenTestSuite.mint(collateral, USER, type(uint96).max); @@ -602,13 +602,12 @@ contract CreditFacadeIntegrationTest is callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (collateral, amount)) }) ), - false, REFERRAL_CODE ); } - /// @dev [FA-10]: decrease debt during openCreditAccount - function test_FA_10_decrease_debt_forbidden_during_openCreditAccount() public { + /// @dev I:[FA-10]: decrease debt during openCreditAccount + function test_I_FA_10_decrease_debt_forbidden_during_openCreditAccount() public { vm.expectRevert(abi.encodeWithSelector(NoPermissionException.selector, DECREASE_DEBT_PERMISSION)); vm.prank(USER); @@ -622,13 +621,12 @@ contract CreditFacadeIntegrationTest is callData: abi.encodeCall(ICreditFacadeMulticall.decreaseDebt, 812) }) ), - false, REFERRAL_CODE ); } - /// @dev [FA-11A]: openCreditAccount reverts if met borrowed limit per block - function test_FA_11A_openCreditAccount_reverts_if_met_borrowed_limit_per_block() public { + /// @dev I:[FA-11A]: openCreditAccount reverts if met borrowed limit per block + function test_I_FA_11A_openCreditAccount_reverts_if_met_borrowed_limit_per_block() public { (uint128 _minDebt, uint128 _maxDebt) = creditFacade.debtLimits(); tokenTestSuite.mint(Tokens.DAI, address(cft.poolMock()), _maxDebt * 2); @@ -652,16 +650,16 @@ contract CreditFacadeIntegrationTest is ); vm.prank(FRIEND); - creditFacade.openCreditAccount(_maxDebt - _minDebt, FRIEND, calls, false, 0); + creditFacade.openCreditAccount(_maxDebt - _minDebt, FRIEND, calls, 0); vm.expectRevert(BorrowedBlockLimitException.selector); vm.prank(USER); - creditFacade.openCreditAccount(_minDebt + 1, USER, calls, false, 0); + creditFacade.openCreditAccount(_minDebt + 1, USER, calls, 0); } - /// @dev [FA-11B]: openCreditAccount reverts if amount < minAmount or amount > maxAmount - function test_FA_11B_openCreditAccount_reverts_if_amount_less_minBorrowedAmount_or_bigger_than_maxBorrowedAmount() + /// @dev I:[FA-11B]: openCreditAccount reverts if amount < minAmount or amount > maxAmount + function test_I_FA_11B_openCreditAccount_reverts_if_amount_less_minBorrowedAmount_or_bigger_than_maxBorrowedAmount() public { (uint128 minBorrowedAmount, uint128 maxBorrowedAmount) = creditFacade.debtLimits(); @@ -675,19 +673,19 @@ contract CreditFacadeIntegrationTest is vm.expectRevert(BorrowAmountOutOfLimitsException.selector); vm.prank(USER); - creditFacade.openCreditAccount(minBorrowedAmount - 1, USER, calls, false, 0); + creditFacade.openCreditAccount(minBorrowedAmount - 1, USER, calls, 0); vm.expectRevert(BorrowAmountOutOfLimitsException.selector); vm.prank(USER); - creditFacade.openCreditAccount(maxBorrowedAmount + 1, USER, calls, false, 0); + creditFacade.openCreditAccount(maxBorrowedAmount + 1, USER, calls, 0); } // // CLOSE CREDIT ACCOUNT // - /// @dev [FA-12]: closeCreditAccount runs multicall operations in correct order - function test_FA_12_closeCreditAccount_runs_operations_in_correct_order() public { + /// @dev I:[FA-12]: closeCreditAccount runs multicall operations in correct order + function test_I_FA_12_closeCreditAccount_runs_operations_in_correct_order() public { (address creditAccount, uint256 balance) = _openTestCreditAccount(); bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); @@ -696,9 +694,7 @@ contract CreditFacadeIntegrationTest is MultiCall({target: address(adapterMock), callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))}) ); - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (creditAccount)) - ); + vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (creditAccount))); vm.expectEmit(true, false, false, false); emit StartMultiCall(creditAccount); @@ -715,9 +711,9 @@ contract CreditFacadeIntegrationTest is vm.expectEmit(false, false, false, true); emit FinishMultiCall(); - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (address(1))) - ); + vm.expectCall(address(botList), abi.encodeCall(BotList.eraseAllBotPermissions, (creditAccount))); + + vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (address(1)))); // vm.expectCall( // address(creditManager), @@ -739,8 +735,8 @@ contract CreditFacadeIntegrationTest is assertEq0(targetMock.callData(), DUMB_CALLDATA, "Incorrect calldata"); } - /// @dev [FA-13]: closeCreditAccount reverts on internal calls in multicall - function test_FA_13_closeCreditAccount_reverts_on_internal_call_in_multicall_on_closure() public { + /// @dev I:[FA-13]: closeCreditAccount reverts on internal calls in multicall + function test_I_FA_13_closeCreditAccount_reverts_on_internal_call_in_multicall_on_closure() public { /// TODO: CHANGE TEST // bytes memory DUMB_CALLDATA = abi.encodeWithSignature("hello(string)", "world"); @@ -762,8 +758,8 @@ contract CreditFacadeIntegrationTest is // LIQUIDATE CREDIT ACCOUNT // - /// @dev [FA-14]: liquidateCreditAccount reverts if hf > 1 - function test_FA_14_liquidateCreditAccount_reverts_if_hf_is_greater_than_1() public { + /// @dev I:[FA-14]: liquidateCreditAccount reverts if hf > 1 + function test_I_FA_14_liquidateCreditAccount_reverts_if_hf_is_greater_than_1() public { (address creditAccount,) = _openTestCreditAccount(); vm.expectRevert(CreditAccountNotLiquidatableException.selector); @@ -772,8 +768,8 @@ contract CreditFacadeIntegrationTest is creditFacade.liquidateCreditAccount(creditAccount, LIQUIDATOR, 0, true, multicallBuilder()); } - /// @dev [FA-15]: liquidateCreditAccount executes needed calls and emits events - function test_FA_15_liquidateCreditAccount_executes_needed_calls_and_emits_events() public { + /// @dev I:[FA-15]: liquidateCreditAccount executes needed calls and emits events + function test_I_FA_15_liquidateCreditAccount_executes_needed_calls_and_emits_events() public { (address creditAccount,) = _openTestCreditAccount(); bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); @@ -786,9 +782,7 @@ contract CreditFacadeIntegrationTest is // EXPECTED STACK TRACE & EVENTS - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (creditAccount)) - ); + vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (creditAccount))); vm.expectEmit(true, false, false, false); emit StartMultiCall(creditAccount); @@ -805,9 +799,9 @@ contract CreditFacadeIntegrationTest is vm.expectEmit(false, false, false, false); emit FinishMultiCall(); - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (address(1))) - ); + vm.expectCall(address(botList), abi.encodeCall(BotList.eraseAllBotPermissions, (creditAccount))); + + vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (address(1)))); // Total value = 2 * DAI_ACCOUNT_AMOUNT, cause we have x2 leverage uint256 totalValue = 2 * DAI_ACCOUNT_AMOUNT; @@ -838,8 +832,8 @@ contract CreditFacadeIntegrationTest is creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 10, true, calls); } - /// @dev [FA-15A]: Borrowing is prohibited after a liquidation with loss - function test_FA_15A_liquidateCreditAccount_prohibits_borrowing_on_loss() public { + /// @dev I:[FA-15A]: Borrowing is prohibited after a liquidation with loss + function test_I_FA_15A_liquidateCreditAccount_prohibits_borrowing_on_loss() public { (address creditAccount,) = _openTestCreditAccount(); uint8 maxDebtPerBlockMultiplier = creditFacade.maxDebtPerBlockMultiplier(); @@ -862,8 +856,8 @@ contract CreditFacadeIntegrationTest is assertEq(maxDebtPerBlockMultiplier, 0, "Increase debt wasn't forbidden after loss"); } - /// @dev [FA-15B]: CreditFacade is paused after too much cumulative loss from liquidations - function test_FA_15B_liquidateCreditAccount_pauses_CreditFacade_on_too_much_loss() public { + /// @dev I:[FA-15B]: CreditFacade is paused after too much cumulative loss from liquidations + function test_I_FA_15B_liquidateCreditAccount_pauses_CreditFacade_on_too_much_loss() public { vm.prank(CONFIGURATOR); creditConfigurator.setMaxCumulativeLoss(1); @@ -883,7 +877,7 @@ contract CreditFacadeIntegrationTest is assertTrue(creditFacade.paused(), "Credit manager was not paused"); } - function test_FA_16_liquidateCreditAccount_reverts_on_internal_call_in_multicall_on_closure() public { + function test_I_FA_16_liquidateCreditAccount_reverts_on_internal_call_in_multicall_on_closure() public { /// TODO: Add all cases with different permissions! MultiCall[] memory calls = multicallBuilder( @@ -904,8 +898,8 @@ contract CreditFacadeIntegrationTest is creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 10, true, calls); } - // [FA-16A]: liquidateCreditAccount reverts when zero address is passed as to - function test_FA_16A_liquidateCreditAccount_reverts_on_zero_to_address() public { + // I:[FA-16A]: liquidateCreditAccount reverts when zero address is passed as to + function test_I_FA_16A_liquidateCreditAccount_reverts_on_zero_to_address() public { bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); MultiCall[] memory calls = multicallBuilder( @@ -926,8 +920,8 @@ contract CreditFacadeIntegrationTest is // INCREASE & DECREASE DEBT // - /// @dev [FA-17]: increaseDebt executes function as expected - function test_FA_17_increaseDebt_executes_actions_as_expected() public { + /// @dev I:[FA-17]: increaseDebt executes function as expected + function test_I_FA_17_increaseDebt_executes_actions_as_expected() public { (address creditAccount,) = _openTestCreditAccount(); vm.expectCall( @@ -957,8 +951,8 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-18A]: increaseDebt revets if more than block limit - function test_FA_18A_increaseDebt_revets_if_more_than_block_limit() public { + /// @dev I:[FA-18A]: increaseDebt revets if more than block limit + function test_I_FA_18A_increaseDebt_revets_if_more_than_block_limit() public { (address creditAccount,) = _openTestCreditAccount(); uint8 maxDebtPerBlockMultiplier = creditFacade.maxDebtPerBlockMultiplier(); @@ -978,8 +972,8 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-18B]: increaseDebt revets if more than maxBorrowedAmount - function test_FA_18B_increaseDebt_revets_if_more_than_block_limit() public { + /// @dev I:[FA-18B]: increaseDebt revets if more than maxBorrowedAmount + function test_I_FA_18B_increaseDebt_revets_if_more_than_block_limit() public { (address creditAccount,) = _openTestCreditAccount(); (, uint128 maxBorrowedAmount) = creditFacade.debtLimits(); @@ -1002,8 +996,8 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-18C]: increaseDebt revets isIncreaseDebtForbidden is enabled - function test_FA_18C_increaseDebt_revets_isIncreaseDebtForbidden_is_enabled() public { + /// @dev I:[FA-18C]: increaseDebt revets isIncreaseDebtForbidden is enabled + function test_I_FA_18C_increaseDebt_revets_isIncreaseDebtForbidden_is_enabled() public { (address creditAccount,) = _openTestCreditAccount(); vm.prank(CONFIGURATOR); @@ -1023,8 +1017,8 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-18D]: increaseDebt reverts if there is a forbidden token on account - function test_FA_18D_increaseDebt_reverts_with_forbidden_tokens() public { + /// @dev I:[FA-18D]: increaseDebt reverts if there is a forbidden token on account + function test_I_FA_18D_increaseDebt_reverts_with_forbidden_tokens() public { (address creditAccount,) = _openTestCreditAccount(); address link = tokenTestSuite.addressOf(Tokens.LINK); @@ -1057,8 +1051,8 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-19]: decreaseDebt executes function as expected - function test_FA_19_decreaseDebt_executes_actions_as_expected() public { + /// @dev I:[FA-19]: decreaseDebt executes function as expected + function test_I_FA_19_decreaseDebt_executes_actions_as_expected() public { (address creditAccount,) = _openTestCreditAccount(); vm.expectCall( @@ -1088,8 +1082,8 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-20]:decreaseDebt revets if less than minBorrowedAmount - function test_FA_20_decreaseDebt_revets_if_less_than_minBorrowedAmount() public { + /// @dev I:[FA-20]:decreaseDebt revets if less than minBorrowedAmount + function test_I_FA_20_decreaseDebt_revets_if_less_than_minBorrowedAmount() public { (address creditAccount,) = _openTestCreditAccount(); (uint128 minBorrowedAmount,) = creditFacade.debtLimits(); @@ -1116,8 +1110,8 @@ contract CreditFacadeIntegrationTest is // ADD COLLATERAL // - /// @dev [FA-21]: addCollateral executes function as expected - function test_FA_21_addCollateral_executes_actions_as_expected() public { + /// @dev I:[FA-21]: addCollateral executes function as expected + function test_I_FA_21_addCollateral_executes_actions_as_expected() public { (address creditAccount,) = _openTestCreditAccount(); expectTokenIsEnabled(creditAccount, Tokens.USDC, false); @@ -1151,8 +1145,8 @@ contract CreditFacadeIntegrationTest is expectTokenIsEnabled(creditAccount, Tokens.USDC, true); } - /// @dev [FA-21C]: addCollateral calls checkEnabledTokensLength - function test_FA_21C_addCollateral_optimizes_enabled_tokens() public { + /// @dev I:[FA-21C]: addCollateral calls checkEnabledTokensLength + function test_I_FA_21C_addCollateral_optimizes_enabled_tokens() public { (address creditAccount,) = _openTestCreditAccount(); vm.prank(USER); @@ -1176,8 +1170,8 @@ contract CreditFacadeIntegrationTest is // MULTICALL // - /// @dev [FA-22]: multicall reverts if calldata length is less than 4 bytes - function test_FA_22_multicall_reverts_if_calldata_length_is_less_than_4_bytes() public { + /// @dev I:[FA-22]: multicall reverts if calldata length is less than 4 bytes + function test_I_FA_22_multicall_reverts_if_calldata_length_is_less_than_4_bytes() public { (address creditAccount,) = _openTestCreditAccount(); vm.expectRevert(IncorrectCallDataException.selector); @@ -1188,8 +1182,8 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-23]: multicall reverts for unknown methods - function test_FA_23_multicall_reverts_for_unknown_methods() public { + /// @dev I:[FA-23]: multicall reverts for unknown methods + function test_I_FA_23_multicall_reverts_for_unknown_methods() public { (address creditAccount,) = _openTestCreditAccount(); bytes memory DUMB_CALLDATA = abi.encodeWithSignature("hello(string)", "world"); @@ -1202,8 +1196,8 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-24]: multicall reverts for creditManager address - function test_FA_24_multicall_reverts_for_creditManager_address() public { + /// @dev I:[FA-24]: multicall reverts for creditManager address + function test_I_FA_24_multicall_reverts_for_creditManager_address() public { (address creditAccount,) = _openTestCreditAccount(); bytes memory DUMB_CALLDATA = abi.encodeWithSignature("hello(string)", "world"); @@ -1216,8 +1210,8 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-25]: multicall reverts on non-adapter targets - function test_FA_25_multicall_reverts_for_non_adapters() public { + /// @dev I:[FA-25]: multicall reverts on non-adapter targets + function test_I_FA_25_multicall_reverts_for_non_adapters() public { (address creditAccount,) = _openTestCreditAccount(); bytes memory DUMB_CALLDATA = abi.encodeWithSignature("hello(string)", "world"); @@ -1229,8 +1223,10 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-26]: multicall addCollateral and oncreaseDebt works with creditFacade calls as expected - function test_FA_26_multicall_addCollateral_and_increase_debt_works_with_creditFacade_calls_as_expected() public { + /// @dev I:[FA-26]: multicall addCollateral and oncreaseDebt works with creditFacade calls as expected + function test_I_FA_26_multicall_addCollateral_and_increase_debt_works_with_creditFacade_calls_as_expected() + public + { (address creditAccount,) = _openTestCreditAccount(); address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); @@ -1286,8 +1282,8 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-27]: multicall addCollateral and decreaseDebt works with creditFacade calls as expected - function test_FA_27_multicall_addCollateral_and_decreaseDebt_works_with_creditFacade_calls_as_expected() public { + /// @dev I:[FA-27]: multicall addCollateral and decreaseDebt works with creditFacade calls as expected + function test_I_FA_27_multicall_addCollateral_and_decreaseDebt_works_with_creditFacade_calls_as_expected() public { (address creditAccount,) = _openTestCreditAccount(); address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); @@ -1343,8 +1339,8 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-28]: multicall reverts for decrease opeartion after increase one - function test_FA_28_multicall_reverts_for_decrease_opeartion_after_increase_one() public { + /// @dev I:[FA-28]: multicall reverts for decrease opeartion after increase one + function test_I_FA_28_multicall_reverts_for_decrease_opeartion_after_increase_one() public { (address creditAccount,) = _openTestCreditAccount(); vm.expectRevert(abi.encodeWithSelector(NoPermissionException.selector, DECREASE_DEBT_PERMISSION)); @@ -1365,8 +1361,8 @@ contract CreditFacadeIntegrationTest is ); } - /// @dev [FA-29]: multicall works with adapters calls as expected - function test_FA_29_multicall_works_with_adapters_calls_as_expected() public { + /// @dev I:[FA-29]: multicall works with adapters calls as expected + function test_I_FA_29_multicall_works_with_adapters_calls_as_expected() public { (address creditAccount,) = _openTestCreditAccount(); bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); @@ -1377,9 +1373,7 @@ contract CreditFacadeIntegrationTest is MultiCall({target: address(adapterMock), callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))}) ); - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (creditAccount)) - ); + vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (creditAccount))); vm.expectEmit(true, true, false, true); emit StartMultiCall(creditAccount); @@ -1396,9 +1390,7 @@ contract CreditFacadeIntegrationTest is vm.expectEmit(false, false, false, true); emit FinishMultiCall(); - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (address(1))) - ); + vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (address(1)))); vm.expectCall( address(creditManager), @@ -1415,9 +1407,9 @@ contract CreditFacadeIntegrationTest is // TRANSFER ACCOUNT OWNERSHIP // - // /// @dev [FA-32]: transferAccountOwnership reverts if "to" user doesn't provide allowance + // /// @dev I:[FA-32]: transferAccountOwnership reverts if "to" user doesn't provide allowance /// TODO: CHANGE TO ALLOWANCE METHOD - // function test_FA_32_transferAccountOwnership_reverts_if_whitelisted_enabled() public { + // function test_I_FA_32_transferAccountOwnership_reverts_if_whitelisted_enabled() public { // cft.testFacadeWithDegenNFT(); // creditFacade = cft.creditFacade(); @@ -1426,8 +1418,8 @@ contract CreditFacadeIntegrationTest is // creditFacade.transferAccountOwnership(DUMB_ADDRESS); // } - /// @dev [FA-33]: transferAccountOwnership reverts if "to" user doesn't provide allowance - function test_FA_33_transferAccountOwnership_reverts_if_to_user_doesnt_provide_allowance() public { + /// @dev I:[FA-33]: transferAccountOwnership reverts if "to" user doesn't provide allowance + function test_I_FA_33_transferAccountOwnership_reverts_if_to_user_doesnt_provide_allowance() public { (address creditAccount,) = _openTestCreditAccount(); vm.expectRevert(AccountTransferNotAllowedException.selector); @@ -1435,8 +1427,8 @@ contract CreditFacadeIntegrationTest is creditFacade.transferAccountOwnership(creditAccount, DUMB_ADDRESS); } - /// @dev [FA-34]: transferAccountOwnership reverts if hf less 1 - function test_FA_34_transferAccountOwnership_reverts_if_hf_less_1() public { + /// @dev I:[FA-34]: transferAccountOwnership reverts if hf less 1 + function test_I_FA_34_transferAccountOwnership_reverts_if_hf_less_1() public { (address creditAccount,) = _openTestCreditAccount(); vm.prank(FRIEND); @@ -1450,8 +1442,8 @@ contract CreditFacadeIntegrationTest is creditFacade.transferAccountOwnership(creditAccount, FRIEND); } - /// @dev [FA-35]: transferAccountOwnership transfers account if it's allowed - function test_FA_35_transferAccountOwnership_transfers_account_if_its_allowed() public { + /// @dev I:[FA-35]: transferAccountOwnership transfers account if it's allowed + function test_I_FA_35_transferAccountOwnership_transfers_account_if_its_allowed() public { (address creditAccount,) = _openTestCreditAccount(); vm.prank(FRIEND); @@ -1472,8 +1464,8 @@ contract CreditFacadeIntegrationTest is // ); } - /// @dev [FA-36]: checkAndUpdateBorrowedBlockLimit doesn't change block limit if maxBorrowedAmountPerBlock = type(uint128).max - function test_FA_36_checkAndUpdateBorrowedBlockLimit_doesnt_change_block_limit_if_set_to_max() public { + /// @dev I:[FA-36]: checkAndUpdateBorrowedBlockLimit doesn't change block limit if maxBorrowedAmountPerBlock = type(uint128).max + function test_I_FA_36_checkAndUpdateBorrowedBlockLimit_doesnt_change_block_limit_if_set_to_max() public { // vm.prank(CONFIGURATOR); // creditConfigurator.setMaxDebtLimitPerBlock(type(uint128).max); @@ -1488,8 +1480,8 @@ contract CreditFacadeIntegrationTest is // assertEq(borrowedInBlock, 0, "Incorrect currentBlockLimit"); } - /// @dev [FA-37]: checkAndUpdateBorrowedBlockLimit doesn't change block limit if maxBorrowedAmountPerBlock = type(uint128).max - function test_FA_37_checkAndUpdateBorrowedBlockLimit_updates_block_limit_properly() public { + /// @dev I:[FA-37]: checkAndUpdateBorrowedBlockLimit doesn't change block limit if maxBorrowedAmountPerBlock = type(uint128).max + function test_I_FA_37_checkAndUpdateBorrowedBlockLimit_updates_block_limit_properly() public { // (uint64 blockLastUpdate, uint128 borrowedInBlock) = creditFacade.getTotalBorrowedInBlock(); // assertEq(blockLastUpdate, 0, "Incorrect blockLastUpdate"); @@ -1540,8 +1532,8 @@ contract CreditFacadeIntegrationTest is // APPROVE ACCOUNT TRANSFER // - /// @dev [FA-38]: approveAccountTransfer changes transfersAllowed - function test_FA_38_transferAccountOwnership_with_allowed_to_transfers_account() public { + /// @dev I:[FA-38]: approveAccountTransfer changes transfersAllowed + function test_I_FA_38_transferAccountOwnership_with_allowed_to_transfers_account() public { assertTrue(creditFacade.transfersAllowed(USER, FRIEND) == false, "Transfer is unexpectedly allowed "); vm.expectEmit(true, true, false, true); @@ -1564,8 +1556,8 @@ contract CreditFacadeIntegrationTest is // ENABLE TOKEN // - /// @dev [FA-39]: enable token works as expected - function test_FA_39_enable_token_is_correct() public { + /// @dev I:[FA-39]: enable token works as expected + function test_I_FA_39_enable_token_is_correct() public { (address creditAccount,) = _openTestCreditAccount(); address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); @@ -1591,8 +1583,8 @@ contract CreditFacadeIntegrationTest is // GETTERS // - /// @dev [FA-41]: calcTotalValue computes correctly - function test_FA_41_calcTotalValue_computes_correctly() public { + /// @dev I:[FA-41]: calcTotalValue computes correctly + function test_I_FA_41_calcTotalValue_computes_correctly() public { (address creditAccount,) = _openTestCreditAccount(); // AFTER OPENING CREDIT ACCOUNT @@ -1651,8 +1643,8 @@ contract CreditFacadeIntegrationTest is // assertEq(tvw, expectedTWV, "Incorrect Threshold weighthed value for 3 asset"); } - /// @dev [FA-42]: calcCreditAccountHealthFactor computes correctly - function test_FA_42_calcCreditAccountHealthFactor_computes_correctly() public { + /// @dev I:[FA-42]: calcCreditAccountHealthFactor computes correctly + function test_I_FA_42_calcCreditAccountHealthFactor_computes_correctly() public { (address creditAccount,) = _openTestCreditAccount(); // AFTER OPENING CREDIT ACCOUNT @@ -1688,8 +1680,8 @@ contract CreditFacadeIntegrationTest is /// CHECK IS ACCOUNT LIQUIDATABLE - /// @dev [FA-44]: setContractToAdapter reverts if called non-configurator - function test_FA_44_config_functions_revert_if_called_non_configurator() public { + /// @dev I:[FA-44]: setContractToAdapter reverts if called non-configurator + function test_I_FA_44_config_functions_revert_if_called_non_configurator() public { vm.expectRevert(CallerNotConfiguratorException.selector); vm.prank(USER); creditFacade.setDebtLimits(100, 100, 100); @@ -1707,8 +1699,8 @@ contract CreditFacadeIntegrationTest is /// [TODO]: add new test - /// @dev [FA-45]: rrevertIfGetLessThan during multicalls works correctly - function test_FA_45_revertIfGetLessThan_works_correctly() public { + /// @dev I:[FA-45]: rrevertIfGetLessThan during multicalls works correctly + function test_I_FA_45_revertIfGetLessThan_works_correctly() public { (address creditAccount,) = _openTestCreditAccount(); uint256 expectedDAI = 1000; @@ -1770,8 +1762,8 @@ contract CreditFacadeIntegrationTest is } } - /// @dev [FA-45A]: rrevertIfGetLessThan everts if called twice - function test_FA_45A_revertIfGetLessThan_reverts_if_called_twice() public { + /// @dev I:[FA-45A]: rrevertIfGetLessThan everts if called twice + function test_I_FA_45A_revertIfGetLessThan_reverts_if_called_twice() public { uint256 expectedDAI = 1000; Balance[] memory expectedBalances = new Balance[](1); @@ -1798,8 +1790,8 @@ contract CreditFacadeIntegrationTest is /// CREDIT FACADE WITH EXPIRATION - /// @dev [FA-46]: openCreditAccount and openCreditAccount no longer work if the CreditFacadeV3 is expired - function test_FA_46_openCreditAccount_reverts_on_expired_CreditFacade() public { + /// @dev I:[FA-46]: openCreditAccount and openCreditAccount no longer work if the CreditFacadeV3 is expired + function test_I_FA_46_openCreditAccount_reverts_on_expired_CreditFacade() public { _setUp({ _underlying: Tokens.DAI, withDegenNFT: false, @@ -1822,13 +1814,12 @@ contract CreditFacadeIntegrationTest is callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) }) ), - false, 0 ); } - /// @dev [FA-47]: liquidateExpiredCreditAccount should not work before the CreditFacadeV3 is expired - function test_FA_47_liquidateExpiredCreditAccount_reverts_before_expiration() public { + /// @dev I:[FA-47]: liquidateExpiredCreditAccount should not work before the CreditFacadeV3 is expired + function test_I_FA_47_liquidateExpiredCreditAccount_reverts_before_expiration() public { _setUp({ _underlying: Tokens.DAI, withDegenNFT: false, @@ -1845,8 +1836,8 @@ contract CreditFacadeIntegrationTest is // creditFacade.liquidateExpiredCreditAccount(USER, LIQUIDATOR, 0, false, multicallBuilder()); } - /// @dev [FA-48]: liquidateExpiredCreditAccount should not work when expiration is set to zero (i.e. CreditFacadeV3 is non-expiring) - function test_FA_48_liquidateExpiredCreditAccount_reverts_on_CreditFacade_with_no_expiration() public { + /// @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 { _openTestCreditAccount(); // vm.expectRevert(CantLiquidateNonExpiredException.selector); @@ -1855,8 +1846,8 @@ contract CreditFacadeIntegrationTest is // creditFacade.liquidateExpiredCreditAccount(USER, LIQUIDATOR, 0, false, multicallBuilder()); } - /// @dev [FA-49]: liquidateExpiredCreditAccount works correctly and emits events - function test_FA_49_liquidateExpiredCreditAccount_works_correctly_after_expiration() public { + /// @dev I:[FA-49]: liquidateExpiredCreditAccount works correctly and emits events + function test_I_FA_49_liquidateExpiredCreditAccount_works_correctly_after_expiration() public { _setUp({ _underlying: Tokens.DAI, withDegenNFT: false, @@ -1876,7 +1867,7 @@ contract CreditFacadeIntegrationTest is vm.roll(block.number + 1); // (uint256 borrowedAmount, uint256 borrowedAmountWithInterest,) = - // creditManager.calcCreditAccountAccruedInterest(creditAccount); + // creditManager.calcAccruedInterestAndFees(creditAccount); // (, uint256 remainingFunds,,) = creditManager.calcClosePayments( // balance, ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT, borrowedAmount, borrowedAmountWithInterest @@ -1884,7 +1875,7 @@ contract CreditFacadeIntegrationTest is // // EXPECTED STACK TRACE & EVENTS - // vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (creditAccount))); + // vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (creditAccount))); // vm.expectEmit(true, false, false, false); // emit StartMultiCall(creditAccount); @@ -1901,7 +1892,7 @@ contract CreditFacadeIntegrationTest is // vm.expectEmit(false, false, false, false); // emit FinishMultiCall(); - // vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (address(1)))); + // vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (address(1)))); // // Total value = 2 * DAI_ACCOUNT_AMOUNT, cause we have x2 leverage // uint256 totalValue = balance; @@ -1936,8 +1927,8 @@ contract CreditFacadeIntegrationTest is /// ENABLE TOKEN /// - /// @dev [FA-53]: enableToken works as expected in a multicall - function test_FA_53_enableToken_works_as_expected_multicall() public { + /// @dev I:[FA-53]: enableToken works as expected in a multicall + function test_I_FA_53_enableToken_works_as_expected_multicall() public { (address creditAccount,) = _openTestCreditAccount(); address token = tokenTestSuite.addressOf(Tokens.USDC); @@ -1960,8 +1951,8 @@ contract CreditFacadeIntegrationTest is expectTokenIsEnabled(creditAccount, Tokens.USDC, true); } - /// @dev [FA-54]: disableToken works as expected in a multicall - function test_FA_54_disableToken_works_as_expected_multicall() public { + /// @dev I:[FA-54]: disableToken works as expected in a multicall + function test_I_FA_54_disableToken_works_as_expected_multicall() public { (address creditAccount,) = _openTestCreditAccount(); address token = tokenTestSuite.addressOf(Tokens.USDC); @@ -1993,8 +1984,8 @@ contract CreditFacadeIntegrationTest is expectTokenIsEnabled(creditAccount, Tokens.USDC, false); } - // /// @dev [FA-56]: liquidateCreditAccount correctly uses BlacklistHelper during liquidations - // function test_FA_56_liquidateCreditAccount_correctly_handles_blacklisted_borrowers() public { + // /// @dev I:[FA-56]: liquidateCreditAccount correctly uses BlacklistHelper during liquidations + // function test_I_FA_56_liquidateCreditAccount_correctly_handles_blacklisted_borrowers() public { // _setUp(Tokens.USDC); // cft.testFacadeWithBlacklistHelper(); @@ -2040,8 +2031,8 @@ contract CreditFacadeIntegrationTest is // assertEq(tokenTestSuite.balanceOf(Tokens.USDC, FRIEND2), expectedAmount, "Transferred amount incorrect"); // } - // /// @dev [FA-57]: openCreditAccount reverts when the borrower is blacklisted on a blacklistable underlying - // function test_FA_57_openCreditAccount_reverts_on_blacklisted_borrower() public { + // /// @dev I:[FA-57]: openCreditAccount reverts when the borrower is blacklisted on a blacklistable underlying + // function test_I_FA_57_openCreditAccount_reverts_on_blacklisted_borrower() public { // _setUp(Tokens.USDC); // cft.testFacadeWithBlacklistHelper(); @@ -2068,20 +2059,18 @@ contract CreditFacadeIntegrationTest is // ); // } - /// @dev [FA-58]: botMulticall works correctly - function test_FA_58_botMulticall_works_correctly() public { - (address creditAccount,) = _openTestCreditAccount(); - - BotList botList = new BotList(address(cft.addressProvider())); + // + // BOT LIST INTEGRATION + // - vm.prank(CONFIGURATOR); - creditConfigurator.setBotList(address(botList)); + /// @dev I:[FA-58]: botMulticall works correctly + function test_I_FA_58_botMulticall_works_correctly() public { + (address creditAccount,) = _openTestCreditAccount(); - /// ???? - address bot = address(new TargetContractMock()); + address bot = address(new GeneralMock()); vm.prank(USER); - botList.setBotPermissions(bot, type(uint192).max); + creditFacade.setBotPermissions(creditAccount, bot, type(uint192).max, uint72(1 ether), uint72(1 ether / 10)); bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); @@ -2089,9 +2078,7 @@ contract CreditFacadeIntegrationTest is MultiCall({target: address(adapterMock), callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))}) ); - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (creditAccount)) - ); + vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (creditAccount))); vm.expectEmit(true, true, false, true); emit StartMultiCall(creditAccount); @@ -2108,9 +2095,7 @@ contract CreditFacadeIntegrationTest is vm.expectEmit(false, false, false, true); emit FinishMultiCall(); - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (address(1))) - ); + vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setActiveCreditAccount, (address(1)))); vm.expectCall( address(creditManager), @@ -2135,8 +2120,45 @@ contract CreditFacadeIntegrationTest is creditFacade.botMulticall(creditAccount, calls); } - /// @dev [FA-59]: setFullCheckParams performs correct full check after multicall - function test_FA_59_setFullCheckParams_correctly_passes_params_to_fullCollateralCheck() public { + /// @dev I:[FA-58A]: setBotPermissions works correctly in CF + function test_I_FA_58A_setBotPermissions_works_correctly() public { + (address creditAccount,) = _openTestCreditAccount(); + + address bot = address(new GeneralMock()); + + vm.expectRevert(CallerNotCreditAccountOwnerException.selector); + vm.prank(FRIEND); + creditFacade.setBotPermissions(creditAccount, bot, type(uint192).max, uint72(1 ether), uint72(1 ether / 10)); + + vm.expectCall(address(botList), abi.encodeCall(BotList.eraseAllBotPermissions, (creditAccount))); + + vm.expectCall( + address(creditManager), + abi.encodeCall(CreditManagerV3.setFlagFor, (creditAccount, BOT_PERMISSIONS_SET_FLAG, true)) + ); + + vm.prank(USER); + creditFacade.setBotPermissions(creditAccount, bot, type(uint192).max, uint72(1 ether), uint72(1 ether / 10)); + + assertTrue(creditManager.flagsOf(creditAccount) & BOT_PERMISSIONS_SET_FLAG > 0, "Flag was not set"); + + vm.expectCall( + address(creditManager), + abi.encodeCall(CreditManagerV3.setFlagFor, (creditAccount, BOT_PERMISSIONS_SET_FLAG, false)) + ); + + vm.prank(USER); + creditFacade.setBotPermissions(creditAccount, bot, 0, 0, 0); + + assertTrue(creditManager.flagsOf(creditAccount) & BOT_PERMISSIONS_SET_FLAG == 0, "Flag was not set"); + } + + // + // FULL CHECK PARAMS + // + + /// @dev I:[FA-59]: setFullCheckParams performs correct full check after multicall + function test_I_FA_59_setFullCheckParams_correctly_passes_params_to_fullCollateralCheck() public { (address creditAccount,) = _openTestCreditAccount(); uint256[] memory collateralHints = new uint256[](1); @@ -2167,8 +2189,8 @@ contract CreditFacadeIntegrationTest is // EMERGENCY LIQUIDATIONS // - /// @dev [FA-62]: addEmergencyLiquidator correctly sets value - function test_FA_62_setEmergencyLiquidator_works_correctly() public { + /// @dev I:[FA-62]: addEmergencyLiquidator correctly sets value + function test_I_FA_62_setEmergencyLiquidator_works_correctly() public { vm.prank(address(creditConfigurator)); creditFacade.setEmergencyLiquidator(DUMB_ADDRESS, AllowanceAction.ALLOW); diff --git a/contracts/test/integration/credit/CreditManager.int.t.sol b/contracts/test/integration/credit/CreditManager.int.t.sol new file mode 100644 index 00000000..dadc921a --- /dev/null +++ b/contracts/test/integration/credit/CreditManager.int.t.sol @@ -0,0 +1,1369 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +import "../../../interfaces/IAddressProviderV3.sol"; +import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; + +import {AccountFactory} from "@gearbox-protocol/core-v2/contracts/core/AccountFactory.sol"; +import {ICreditAccount} from "@gearbox-protocol/core-v2/contracts/interfaces/ICreditAccount.sol"; +import { + ICreditManagerV3, + ICreditManagerV3Events, + ClosureAction, + CollateralTokenData, + ManageDebtAction +} from "../../../interfaces/ICreditManagerV3.sol"; + +import {IPriceOracleV2, IPriceOracleV2Ext} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; +import {IWETHGateway} from "../../../interfaces/IWETHGateway.sol"; +import {IWithdrawalManager} from "../../../interfaces/IWithdrawalManager.sol"; + +import {CreditManagerV3} from "../../../credit/CreditManagerV3.sol"; + +import {IPoolService} from "@gearbox-protocol/core-v2/contracts/interfaces/IPoolService.sol"; + +import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20Mock} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/ERC20Mock.sol"; +import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; + +// LIBS & TRAITS +import {BitMask} from "../../../libraries/BitMask.sol"; +// TESTS + +import "../../lib/constants.sol"; +import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; + +// EXCEPTIONS +import {TokenAlreadyAddedException} from "../../../interfaces/IExceptions.sol"; + +// MOCKS +import {PriceFeedMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/oracles/PriceFeedMock.sol"; +import {PoolMock} from "../../mocks//pool/PoolMock.sol"; +import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/adapters/TargetContractMock.sol"; +import { + ERC20ApproveRestrictedRevert, + ERC20ApproveRestrictedFalse +} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/ERC20ApproveRestricted.sol"; + +// SUITES +import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; +import {Tokens} from "../../config/Tokens.sol"; +import {CreditManagerTestSuite} from "../../suites/CreditManagerTestSuite.sol"; + +import {CreditConfig} from "../../config/CreditConfig.sol"; + +// EXCEPTIONS +import "../../../interfaces/IExceptions.sol"; + +import {Test} from "forge-std/Test.sol"; +import "forge-std/console.sol"; + +/// @title AddressRepository +/// @notice Stores addresses of deployed contracts +contract CreditManagerIntegrationTest is Test, ICreditManagerV3Events, BalanceHelper { + using BitMask for uint256; + + CreditManagerTestSuite cms; + + IAddressProviderV3 addressProvider; + IWETH wethToken; + + AccountFactory af; + CreditManagerV3 creditManager; + PoolMock poolMock; + IPriceOracleV2 priceOracle; + IWETHGateway wethGateway; + IWithdrawalManager withdrawalManager; + ACL acl; + address underlying; + + CreditConfig creditConfig; + + function setUp() public { + tokenTestSuite = new TokensTestSuite(); + + tokenTestSuite.topUpWETH{value: 100 * WAD}(); + _connectCreditManagerSuite(Tokens.DAI, false); + } + + /// + /// HELPERS + + function _connectCreditManagerSuite(Tokens t, bool internalSuite) internal { + creditConfig = new CreditConfig(tokenTestSuite, t); + cms = new CreditManagerTestSuite(creditConfig, internalSuite, false, 1); + + acl = cms.acl(); + + addressProvider = cms.addressProvider(); + af = cms.af(); + + poolMock = cms.poolMock(); + withdrawalManager = cms.withdrawalManager(); + + creditManager = cms.creditManager(); + + priceOracle = IPriceOracleV2(creditManager.priceOracle()); + underlying = creditManager.underlying(); + wethGateway = IWETHGateway(creditManager.wethGateway()); + } + + /// @dev Opens credit account for testing management functions + function _openCreditAccount() + internal + returns ( + uint256 borrowedAmount, + uint256 cumulativeIndexLastUpdate, + uint256 cumulativeIndexAtClose, + address creditAccount + ) + { + return cms.openCreditAccount(); + } + + function mintBalance(address creditAccount, Tokens t, uint256 amount, bool enable) internal { + tokenTestSuite.mint(t, creditAccount, amount); + // if (enable) { + // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(t)); + // } + } + + function _addAndEnableTokens(address creditAccount, uint256 numTokens, uint256 balance) internal { + for (uint256 i = 0; i < numTokens; i++) { + ERC20Mock t = new ERC20Mock("new token", "nt", 18); + PriceFeedMock pf = new PriceFeedMock(10**8, 8); + + vm.startPrank(CONFIGURATOR); + creditManager.addToken(address(t)); + IPriceOracleV2Ext(address(priceOracle)).addPriceFeed(address(t), address(pf)); + creditManager.setCollateralTokenData(address(t), 8000, 8000, type(uint40).max, 0); + vm.stopPrank(); + + t.mint(creditAccount, balance); + + // creditManager.checkAndEnableToken(address(t)); + } + } + + function _getRandomBits(uint256 ones, uint256 zeros, uint256 randomValue) + internal + pure + returns (bool[] memory result, uint256 breakPoint) + { + if ((ones + zeros) == 0) { + result = new bool[](0); + breakPoint = 0; + return (result, breakPoint); + } + + uint256 onesCurrent = ones; + uint256 zerosCurrent = zeros; + + result = new bool[](ones + zeros); + uint256 i = 0; + + while (onesCurrent + zerosCurrent > 0) { + uint256 rand = uint256(keccak256(abi.encodePacked(randomValue))) % (onesCurrent + zerosCurrent); + if (rand < onesCurrent) { + result[i] = true; + onesCurrent--; + } else { + result[i] = false; + zerosCurrent--; + } + + i++; + } + + if (ones > 0) { + uint256 breakpointCounter = (uint256(keccak256(abi.encodePacked(randomValue))) % (ones)) + 1; + + for (uint256 j = 0; j < result.length; j++) { + if (result[j]) { + breakpointCounter--; + } + + if (breakpointCounter == 0) { + breakPoint = j; + break; + } + } + } + } + + function _baseFullCollateralCheck(address creditAccount) internal { + // TODO: CHANGE + creditManager.fullCollateralCheck(creditAccount, 0, new uint256[](0), 10000); + } + + /// + /// OPEN CREDIT ACCOUNT + /// + + /// @dev I:[CM-1]: openCreditAccount transfers_tokens_from_pool + function test_I_CM_01_openCreditAccount_transfers_tokens_from_pool() public { + address expectedCreditAccount = AccountFactory(addressProvider.getAddressOrRevert(AP_ACCOUNT_FACTORY, 1)).head(); + + uint256 blockAtOpen = block.number; + uint256 cumulativeAtOpen = 1012; + poolMock.setCumulativeIndexNow(cumulativeAtOpen); + + // Existing address case + address creditAccount = creditManager.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER); + assertEq(creditAccount, expectedCreditAccount, "Incorrecct credit account address"); + + (uint256 debt, uint256 cumulativeIndexLastUpdate,,,,) = creditManager.creditAccountInfo(creditAccount); + + assertEq(debt, DAI_ACCOUNT_AMOUNT, "Incorrect borrowed amount set in CA"); + assertEq(cumulativeIndexLastUpdate, cumulativeAtOpen, "Incorrect cumulativeIndexLastUpdate set in CA"); + + assertEq(ICreditAccount(creditAccount).since(), blockAtOpen, "Incorrect since set in CA"); + + expectBalance(Tokens.DAI, creditAccount, DAI_ACCOUNT_AMOUNT); + assertEq(poolMock.lendAmount(), DAI_ACCOUNT_AMOUNT, "Incorrect DAI_ACCOUNT_AMOUNT in Pool call"); + assertEq(poolMock.lendAccount(), creditAccount, "Incorrect credit account in lendCreditAccount call"); + // assertEq(creditManager.creditAccounts(USER), creditAccount, "Credit account is not associated with user"); + assertEq(creditManager.enabledTokensMaskOf(creditAccount), 0, "Incorrect enabled token mask"); + } + + // + // CLOSE CREDIT ACCOUNT + // + + /// @dev I:[CM-9]: closeCreditAccount updates pool correctly + /// remove borrower from creditAccounts mapping + function test_I_CM_09_close_credit_account_updates_pool_correctly() public { + (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); + + assertTrue( + creditAccount != AccountFactory(addressProvider.getAddressOrRevert(AP_ACCOUNT_FACTORY, 1)).tail(), + "credit account is already in tail!" + ); + + // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount + tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); + + // Increase block number cause it's forbidden to close credit account in the same block + vm.roll(block.number + 1); + + // creditManager.closeCreditAccount( + // creditAccount, ClosureAction.CLOSE_ACCOUNT, 0, USER, USER, 0, 0, DAI_ACCOUNT_AMOUNT, false + // ); + + assertEq( + creditAccount, + AccountFactory(addressProvider.getAddressOrRevert(AP_ACCOUNT_FACTORY, 1)).tail(), + "credit account is not in accountFactory tail!" + ); + + // vm.expectRevert(CreditAccountNotExistsException.selector); + // creditManager.getCreditAccountOrRevert(USER); + } + + /// @dev I:[CM-10]: 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_CM_10_close_credit_account_returns_underlying_token_if_not_liquidated() public { + ( + uint256 borrowedAmount, + uint256 cumulativeIndexLastUpdate, + uint256 cumulativeIndexAtClose, + address creditAccount + ) = _openCreditAccount(); + + uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(poolMock)); + + // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount + tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); + + uint256 interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; + + (uint16 feeInterest,,,,) = creditManager.fees(); + + uint256 profit = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; + + uint256 amountToPool = borrowedAmount + interestAccrued + profit; + + vm.expectCall( + address(poolMock), + abi.encodeWithSelector(IPoolService.repayCreditAccount.selector, borrowedAmount, profit, 0) + ); + + // (uint256 remainingFunds, uint256 loss) = creditManager.closeCreditAccount( + // creditAccount, + // ClosureAction.CLOSE_ACCOUNT, + // 0, + // USER, + // FRIEND, + // 1, + // 0, + // DAI_ACCOUNT_AMOUNT + interestAccrued, + // false + // ); + + // assertEq(remainingFunds, 0, "Remaining funds is not zero!"); + + // assertEq(loss, 0, "Loss is not zero"); + + expectBalance(Tokens.DAI, creditAccount, 1); + + expectBalance(Tokens.DAI, address(poolMock), poolBalanceBefore + amountToPool); + + expectBalance(Tokens.DAI, FRIEND, 2 * borrowedAmount - amountToPool - 1, "Incorrect amount were paid back"); + } + + /// @dev I:[CM-11]: 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_CM_11_close_credit_account_charges_caller_if_underlying_token_not_enough() public { + ( + uint256 borrowedAmount, + uint256 cumulativeIndexLastUpdate, + uint256 cumulativeIndexAtClose, + address creditAccount + ) = _openCreditAccount(); + + uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(poolMock)); + + // Transfer funds to USER account to be able to cover extra cost + tokenTestSuite.mint(Tokens.DAI, USER, borrowedAmount); + + uint256 interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; + + (uint16 feeInterest,,,,) = creditManager.fees(); + + uint256 profit = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; + + uint256 amountToPool = borrowedAmount + interestAccrued + profit; + + vm.expectCall( + address(poolMock), + abi.encodeWithSelector(IPoolService.repayCreditAccount.selector, borrowedAmount, profit, 0) + ); + + // (uint256 remainingFunds, uint256 loss) = creditManager.closeCreditAccount( + // creditAccount, + // ClosureAction.CLOSE_ACCOUNT, + // 0, + // USER, + // FRIEND, + // 1, + // 0, + // DAI_ACCOUNT_AMOUNT + interestAccrued, + // false + // ); + // assertEq(remainingFunds, 0, "Remaining funds is not zero!"); + + // assertEq(loss, 0, "Loss is not zero"); + + expectBalance(Tokens.DAI, creditAccount, 1, "Credit account balance != 1"); + + expectBalance(Tokens.DAI, address(poolMock), poolBalanceBefore + amountToPool); + + expectBalance(Tokens.DAI, USER, 2 * borrowedAmount - amountToPool - 1, "Incorrect amount were paid back"); + + expectBalance(Tokens.DAI, FRIEND, 0, "Incorrect amount were paid back"); + } + + /// @dev I:[CM-12]: closeCreditAccount 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_CM_12_close_credit_account_charges_caller_if_underlying_token_not_enough() public { + for (uint256 i = 0; i < 2; i++) { + uint256 friendBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, FRIEND); + + ClosureAction action = i == 1 ? ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT : ClosureAction.LIQUIDATE_ACCOUNT; + uint256 interestAccrued; + uint256 borrowedAmount; + address creditAccount; + + { + uint256 cumulativeIndexLastUpdate; + uint256 cumulativeIndexAtClose; + (borrowedAmount, cumulativeIndexLastUpdate, cumulativeIndexAtClose, creditAccount) = + _openCreditAccount(); + + interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; + } + + uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(poolMock)); + uint256 discount; + + { + (,, uint16 liquidationDiscount,, uint16 liquidationDiscountExpired) = creditManager.fees(); + discount = action == ClosureAction.LIQUIDATE_ACCOUNT ? liquidationDiscount : liquidationDiscountExpired; + } + + // uint256 totalValue = borrowedAmount; + uint256 amountToPool = (borrowedAmount * discount) / PERCENTAGE_FACTOR; + + { + uint256 loss = borrowedAmount + interestAccrued - amountToPool; + + vm.expectCall( + address(poolMock), + abi.encodeWithSelector(IPoolService.repayCreditAccount.selector, borrowedAmount, 0, loss) + ); + } + { + uint256 a = borrowedAmount + interestAccrued; + + // (uint256 remainingFunds,) = creditManager.closeCreditAccount( + // creditAccount, action, borrowedAmount, LIQUIDATOR, FRIEND, 1, 0, a, false + // ); + } + + expectBalance(Tokens.DAI, creditAccount, 1, "Credit account balance != 1"); + + expectBalance(Tokens.DAI, address(poolMock), poolBalanceBefore + amountToPool); + + expectBalance( + Tokens.DAI, + FRIEND, + friendBalanceBefore + (borrowedAmount * (PERCENTAGE_FACTOR - discount)) / PERCENTAGE_FACTOR + - (i == 2 ? 0 : 1), + "Incorrect amount were paid to liqiudator friend address" + ); + } + } + + /// @dev I:[CM-13]: openCreditAccount 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_CM_13_close_credit_account_charges_caller_if_underlying_token_not_enough() public { + for (uint256 i = 0; i < 2; i++) { + setUp(); + uint256 borrowedAmount; + address creditAccount; + + uint256 expectedRemainingFunds = 100 * WAD; + + uint256 profit; + uint256 amountToPool; + uint256 totalValue; + uint256 interestAccrued; + { + uint256 cumulativeIndexLastUpdate; + uint256 cumulativeIndexAtClose; + (borrowedAmount, cumulativeIndexLastUpdate, cumulativeIndexAtClose, creditAccount) = + _openCreditAccount(); + + interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; + + uint16 feeInterest; + uint16 feeLiquidation; + uint16 liquidationDiscount; + + { + (feeInterest,,,,) = creditManager.fees(); + } + + { + uint16 feeLiquidationNormal; + uint16 feeLiquidationExpired; + + (, feeLiquidationNormal,, feeLiquidationExpired,) = creditManager.fees(); + + feeLiquidation = (i == 0 || i == 2) ? feeLiquidationNormal : feeLiquidationExpired; + } + + { + uint16 liquidationDiscountNormal; + uint16 liquidationDiscountExpired; + + (feeInterest,, liquidationDiscountNormal,, liquidationDiscountExpired) = creditManager.fees(); + + liquidationDiscount = i == 1 ? liquidationDiscountExpired : liquidationDiscountNormal; + } + + uint256 profitInterest = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; + + amountToPool = borrowedAmount + 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(poolMock)); + + 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, borrowedAmount, "creditAccount has incorrect initial balance"); + + vm.expectCall( + address(poolMock), + abi.encodeWithSelector(IPoolService.repayCreditAccount.selector, borrowedAmount, profit, 0) + ); + + uint256 remainingFunds; + + { + uint256 loss; + + uint256 a = borrowedAmount + interestAccrued; + // (remainingFunds, loss) = creditManager.closeCreditAccount( + // creditAccount, + // i == 1 ? ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT : ClosureAction.LIQUIDATE_ACCOUNT, + // totalValue, + // LIQUIDATOR, + // FRIEND, + // 1, + // 0, + // a, + // 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(poolMock), poolBalanceBefore + amountToPool, "INCORRECT POOL BALANCE"); + } + + expectBalance( + Tokens.DAI, + LIQUIDATOR, + totalValue + borrowedAmount - amountToPool - remainingFunds - 1, + "Incorrect amount were paid to lqiudaidator" + ); + } + } + + /// @dev I:[CM-14]: 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_CM_14_close_credit_account_with_nonzero_skipTokenMask_sends_correct_tokens() public { + (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); + creditManager.transferAccountOwnership(creditAccount, address(this)); + + tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); + tokenTestSuite.mint(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); + // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.WETH)); + + tokenTestSuite.mint(Tokens.USDC, creditAccount, USDC_EXCHANGE_AMOUNT); + // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.USDC)); + + tokenTestSuite.mint(Tokens.LINK, creditAccount, LINK_EXCHANGE_AMOUNT); + // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.LINK)); + + uint256 wethTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); + uint256 usdcTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)); + uint256 linkTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); + + creditManager.transferAccountOwnership(creditAccount, USER); + + // creditManager.closeCreditAccount( + // creditAccount, + // ClosureAction.CLOSE_ACCOUNT, + // 0, + // USER, + // FRIEND, + // wethTokenMask | usdcTokenMask | linkTokenMask, + // wethTokenMask | usdcTokenMask, + // DAI_ACCOUNT_AMOUNT, + // 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:[CM-16]: closeCreditAccount sends ETH for WETH creditManger to borrower + /// CASE: CLOSURE + /// Underlying token: WETH + function test_I_CM_16_close_weth_credit_account_sends_eth_to_borrower() public { + // It takes "clean" address which doesn't holds any assets + + _connectCreditManagerSuite(Tokens.WETH, false); + + /// CLOSURE CASE + ( + uint256 borrowedAmount, + uint256 cumulativeIndexLastUpdate, + uint256 cumulativeIndexAtClose, + address creditAccount + ) = _openCreditAccount(); + + // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount + tokenTestSuite.mint(Tokens.WETH, creditAccount, borrowedAmount); + + uint256 interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; + + // creditManager.closeCreditAccount(USER, ClosureAction.CLOSE_ACCOUNT, 0, USER, USER, 0, true); + + // creditManager.closeCreditAccount( + // creditAccount, ClosureAction.CLOSE_ACCOUNT, 0, USER, USER, 1, 0, borrowedAmount + interestAccrued, true + // ); + + expectBalance(Tokens.WETH, creditAccount, 1); + + (uint16 feeInterest,,,,) = creditManager.fees(); + + uint256 profit = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; + + uint256 amountToPool = borrowedAmount + interestAccrued + profit; + + assertEq( + wethGateway.balanceOf(USER), + 2 * borrowedAmount - amountToPool - 1, + "Incorrect amount deposited on wethGateway" + ); + } + + /// @dev I:[CM-17]: closeCreditAccount sends ETH for WETH creditManger to borrower + /// CASE: CLOSURE + /// Underlying token: DAI + function test_I_CM_17_close_dai_credit_account_sends_eth_to_borrower() public { + /// CLOSURE CASE + (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); + creditManager.transferAccountOwnership(creditAccount, address(this)); + + // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount + tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); + + // 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)); + + creditManager.transferAccountOwnership(creditAccount, USER); + // creditManager.closeCreditAccount( + // creditAccount, + // ClosureAction.CLOSE_ACCOUNT, + // 0, + // USER, + // USER, + // wethTokenMask | daiTokenMask, + // 0, + // borrowedAmount, + // true + // ); + + expectBalance(Tokens.WETH, creditAccount, 1); + + assertEq(wethGateway.balanceOf(USER), WETH_EXCHANGE_AMOUNT - 1, "Incorrect amount deposited on wethGateway"); + } + + /// @dev I:[CM-18]: closeCreditAccount sends ETH for WETH creditManger to borrower + /// CASE: LIQUIDATION + function test_I_CM_18_close_credit_account_sends_eth_to_liquidator_and_weth_to_borrower() public { + /// 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, false); + + /// CLOSURE CASE + (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); + + // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount + tokenTestSuite.mint(Tokens.WETH, creditAccount, borrowedAmount); + + uint256 totalValue = borrowedAmount * 2; + + uint256 wethTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); + uint256 daiTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)); + + // (uint256 remainingFunds,) = creditManager.closeCreditAccount( + // creditAccount, + // ClosureAction.LIQUIDATE_ACCOUNT, + // totalValue, + // LIQUIDATOR, + // FRIEND, + // wethTokenMask | daiTokenMask, + // 0, + // borrowedAmount, + // 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( + wethGateway.balanceOf(FRIEND), + (totalValue * (PERCENTAGE_FACTOR - liquidationDiscount)) / PERCENTAGE_FACTOR, + "Incorrect amount were paid to liqiudator friend address" + ); + } + + /// @dev I:[CM-19]: closeCreditAccount sends ETH for WETH creditManger to borrower + /// CASE: LIQUIDATION + /// Underlying token: DAI + function test_I_CM_19_close_dai_credit_account_sends_eth_to_liquidator() public { + /// CLOSURE CASE + (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); + creditManager.transferAccountOwnership(creditAccount, address(this)); + + // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount + tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); + + // 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)); + + // (uint256 remainingFunds,) = creditManager.closeCreditAccount( + // creditAccount, + // ClosureAction.LIQUIDATE_ACCOUNT, + // borrowedAmount, + // LIQUIDATOR, + // FRIEND, + // wethTokenMask | daiTokenMask, + // 0, + // borrowedAmount, + // true + // ); + + expectBalance(Tokens.WETH, creditAccount, 1); + + assertEq( + wethGateway.balanceOf(FRIEND), + WETH_EXCHANGE_AMOUNT - 1, + "Incorrect amount were paid to liqiudator friend address" + ); + } + + // + // MANAGE DEBT + // + + /// @dev I:[CM-20]: manageDebt correctly increases debt + function test_I_CM_20_manageDebt_correctly_increases_debt(uint128 amount) public { + (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate,, address creditAccount) = cms.openCreditAccount(1); + + tokenTestSuite.mint(Tokens.DAI, address(poolMock), amount); + + poolMock.setCumulativeIndexNow(cumulativeIndexLastUpdate * 2); + + uint256 expectedNewCulumativeIndex = + (2 * cumulativeIndexLastUpdate * (borrowedAmount + amount)) / (2 * borrowedAmount + amount); + + (uint256 newBorrowedAmount,,) = + creditManager.manageDebt(creditAccount, amount, 1, ManageDebtAction.INCREASE_DEBT); + + assertEq(newBorrowedAmount, borrowedAmount + amount, "Incorrect returned newBorrowedAmount"); + + // assertLe( + // (ICreditAccount(creditAccount).cumulativeIndexLastUpdate() * (10 ** 6)) / expectedNewCulumativeIndex, + // 10 ** 6, + // "Incorrect cumulative index" + // ); + + (uint256 debt,,,,,) = creditManager.creditAccountInfo(creditAccount); + assertEq(debt, newBorrowedAmount, "Incorrect borrowedAmount"); + + expectBalance(Tokens.DAI, creditAccount, newBorrowedAmount, "Incorrect balance on credit account"); + + assertEq(poolMock.lendAmount(), amount, "Incorrect lend amount"); + + assertEq(poolMock.lendAccount(), creditAccount, "Incorrect lend account"); + } + + /// @dev I:[CM-21]: manageDebt correctly decreases debt + function test_I_CM_21_manageDebt_correctly_decreases_debt(uint128 amount) public { + // tokenTestSuite.mint(Tokens.DAI, address(poolMock), (uint256(type(uint128).max) * 14) / 10); + + // (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow, address creditAccount) = + // cms.openCreditAccount((uint256(type(uint128).max) * 14) / 10); + + // (,, uint256 totalDebt) = creditManager.calcAccruedInterestAndFees(creditAccount); + + // uint256 expectedInterestAndFees; + // uint256 expectedBorrowAmount; + // if (amount >= totalDebt - borrowedAmount) { + // expectedInterestAndFees = 0; + // expectedBorrowAmount = totalDebt - amount; + // } else { + // expectedInterestAndFees = totalDebt - borrowedAmount - amount; + // expectedBorrowAmount = borrowedAmount; + // } + + // (uint256 newBorrowedAmount,) = + // creditManager.manageDebt(creditAccount, amount, 1, ManageDebtAction.DECREASE_DEBT); + + // assertEq(newBorrowedAmount, expectedBorrowAmount, "Incorrect returned newBorrowedAmount"); + + // if (amount >= totalDebt - borrowedAmount) { + // (,, uint256 newTotalDebt) = creditManager.calcAccruedInterestAndFees(creditAccount); + + // assertEq(newTotalDebt, newBorrowedAmount, "Incorrect new interest"); + // } else { + // (,, uint256 newTotalDebt) = creditManager.calcAccruedInterestAndFees(creditAccount); + + // assertLt( + // (RAY * (newTotalDebt - newBorrowedAmount)) / expectedInterestAndFees - RAY, + // 10000, + // "Incorrect new interest" + // ); + // } + // uint256 cumulativeIndexLastUpdateAfter; + // { + // uint256 debt; + // (debt, cumulativeIndexLastUpdateAfter,,,,) = creditManager.creditAccountInfo(creditAccount); + + // assertEq(debt, newBorrowedAmount, "Incorrect borrowedAmount"); + // } + + // expectBalance(Tokens.DAI, creditAccount, borrowedAmount - amount, "Incorrect balance on credit account"); + + // if (amount >= totalDebt - borrowedAmount) { + // assertEq(cumulativeIndexLastUpdateAfter, cumulativeIndexNow, "Incorrect cumulativeIndexLastUpdate"); + // } else { + // CreditManagerTestInternal cmi = new CreditManagerTestInternal( + // creditManager.poolService(), address(withdrawalManager) + // ); + + // { + // (uint256 feeInterest,,,,) = creditManager.fees(); + // amount = uint128((uint256(amount) * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR + feeInterest)); + // } + + // assertEq( + // cumulativeIndexLastUpdateAfter, + // cmi.calcNewCumulativeIndex(borrowedAmount, amount, cumulativeIndexNow, cumulativeIndexLastUpdate, false), + // "Incorrect cumulativeIndexLastUpdate" + // ); + // } + } + + // + // ADD COLLATERAL + // + + // + // APPROVE CREDIT ACCOUNT + // + + /// @dev I:[CM-25A]: approveCreditAccount reverts if the token is not added + function test_I_CM_25A_approveCreditAccount_reverts_if_the_token_is_not_added() public { + (,,, address creditAccount) = _openCreditAccount(); + creditManager.setActiveCreditAccount(creditAccount); + + vm.prank(CONFIGURATOR); + creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); + + vm.expectRevert(TokenNotAllowedException.selector); + + vm.prank(ADAPTER); + creditManager.approveCreditAccount(DUMB_ADDRESS, 100); + } + + /// @dev I:[CM-26]: approveCreditAccount approves with desired allowance + function test_I_CM_26_approveCreditAccount_approves_with_desired_allowance() public { + (,,, address creditAccount) = _openCreditAccount(); + creditManager.setActiveCreditAccount(creditAccount); + + vm.prank(CONFIGURATOR); + creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); + + // Case, when current allowance > Allowance_THRESHOLD + tokenTestSuite.approve(Tokens.DAI, creditAccount, DUMB_ADDRESS, 200); + + address dai = tokenTestSuite.addressOf(Tokens.DAI); + + vm.prank(ADAPTER); + creditManager.approveCreditAccount(dai, DAI_EXCHANGE_AMOUNT); + + expectAllowance(Tokens.DAI, creditAccount, DUMB_ADDRESS, DAI_EXCHANGE_AMOUNT); + } + + /// @dev I:[CM-27A]: approveCreditAccount works for ERC20 that revert if allowance > 0 before approve + function test_I_CM_27A_approveCreditAccount_works_for_ERC20_with_approve_restrictions() public { + (,,, address creditAccount) = _openCreditAccount(); + creditManager.setActiveCreditAccount(creditAccount); + + vm.prank(CONFIGURATOR); + creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); + + address approveRevertToken = address(new ERC20ApproveRestrictedRevert()); + + vm.prank(CONFIGURATOR); + creditManager.addToken(approveRevertToken); + + vm.prank(ADAPTER); + creditManager.approveCreditAccount(approveRevertToken, DAI_EXCHANGE_AMOUNT); + + vm.prank(ADAPTER); + creditManager.approveCreditAccount(approveRevertToken, 2 * DAI_EXCHANGE_AMOUNT); + + expectAllowance(approveRevertToken, creditAccount, DUMB_ADDRESS, 2 * DAI_EXCHANGE_AMOUNT); + } + + // /// @dev I:[CM-27B]: approveCreditAccount works for ERC20 that returns false if allowance > 0 before approve + function test_I_CM_27B_approveCreditAccount_works_for_ERC20_with_approve_restrictions() public { + (,,, address creditAccount) = _openCreditAccount(); + creditManager.setActiveCreditAccount(creditAccount); + + address approveFalseToken = address(new ERC20ApproveRestrictedFalse()); + + vm.prank(CONFIGURATOR); + creditManager.addToken(approveFalseToken); + + vm.prank(CONFIGURATOR); + creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); + + vm.prank(ADAPTER); + creditManager.approveCreditAccount(approveFalseToken, DAI_EXCHANGE_AMOUNT); + + vm.prank(ADAPTER); + creditManager.approveCreditAccount(approveFalseToken, 2 * DAI_EXCHANGE_AMOUNT); + + expectAllowance(approveFalseToken, creditAccount, DUMB_ADDRESS, 2 * DAI_EXCHANGE_AMOUNT); + } + + // + // EXECUTE ORDER + // + + /// @dev I:[CM-29]: executeOrder calls credit account method and emit event + function test_I_CM_29_executeOrder_calls_credit_account_method_and_emit_event() public { + (,,, address creditAccount) = _openCreditAccount(); + creditManager.setActiveCreditAccount(creditAccount); + + TargetContractMock targetMock = new TargetContractMock(); + + vm.prank(CONFIGURATOR); + creditManager.setContractAllowance(ADAPTER, address(targetMock)); + + bytes memory callData = bytes("Hello, world!"); + + // we emit the event we expect to see. + vm.expectEmit(true, false, false, false); + emit ExecuteOrder(address(targetMock)); + + // stack trace check + vm.expectCall(creditAccount, abi.encodeWithSignature("execute(address,bytes)", address(targetMock), callData)); + vm.expectCall(address(targetMock), callData); + + vm.prank(ADAPTER); + creditManager.executeOrder(callData); + + assertEq0(targetMock.callData(), callData, "Incorrect calldata"); + } + + // + // TRASNFER ASSETS TO + // + + // /// @dev I:[CM-44]: _transferAssetsTo sends all tokens except underlying one and not-enabled to provided address + // function test_I_CM_44_transferAssetsTo_sends_all_tokens_except_underlying_one_to_provided_address() public { + // // It enables CreditManagerTestInternal for some test cases + // _connectCreditManagerSuite(Tokens.DAI, true); + + // address[2] memory friends = [FRIEND, FRIEND2]; + + // // CASE 0: convertToETH = false + // // CASE 1: convertToETH = true + // for (uint256 i = 0; i < 2; i++) { + // bool convertToETH = i > 0; + + // address friend = friends[i]; + // (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); + // creditManager.transferAccountOwnership(creditAccount, address(this)); + + // CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); + + // tokenTestSuite.mint(Tokens.USDC, creditAccount, USDC_EXCHANGE_AMOUNT); + // tokenTestSuite.mint(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); + // tokenTestSuite.mint(Tokens.LINK, creditAccount, LINK_EXCHANGE_AMOUNT); + + // address wethTokenAddr = tokenTestSuite.addressOf(Tokens.WETH); + // // creditManager.checkAndEnableToken(wethTokenAddr); + + // uint256 enabledTokensMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)) + // | creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); + + // cmi.batchTokensTransfer(creditAccount, friend, convertToETH, enabledTokensMask); + + // expectBalance(Tokens.DAI, creditAccount, borrowedAmount, "Underlying assets were transffered!"); + + // expectBalance(Tokens.DAI, friend, 0); + + // expectBalance(Tokens.USDC, creditAccount, USDC_EXCHANGE_AMOUNT); + + // expectBalance(Tokens.USDC, friend, 0); + + // expectBalance(Tokens.WETH, creditAccount, 1); + + // if (convertToETH) { + // assertEq( + // wethGateway.balanceOf(friend), + // WETH_EXCHANGE_AMOUNT - 1, + // "Incorrect amount were sent to friend address" + // ); + // } else { + // expectBalance(Tokens.WETH, friend, WETH_EXCHANGE_AMOUNT - 1); + // } + + // expectBalance(Tokens.LINK, creditAccount, LINK_EXCHANGE_AMOUNT); + + // expectBalance(Tokens.LINK, friend, 0); + + // creditManager.transferAccountOwnership(creditAccount, USER); + // // creditManager.closeCreditAccount( + // // creditAccount, + // // ClosureAction.LIQUIDATE_ACCOUNT, + // // 0, + // // LIQUIDATOR, + // // friend, + // // enabledTokensMask, + // // 0, + // // DAI_ACCOUNT_AMOUNT, + // // false + // // ); + // } + // } + + // + // SAFE TOKEN TRANSFER + // + + // /// @dev I:[CM-45]: _safeTokenTransfer transfers tokens + // function test_I_CM_45_safeTokenTransfer_transfers_tokens() public { + // // It enables CreditManagerTestInternal for some test cases + // _connectCreditManagerSuite(Tokens.DAI, true); + + // uint256 WETH_TRANSFER = WETH_EXCHANGE_AMOUNT / 4; + + // address[2] memory friends = [FRIEND, FRIEND2]; + + // // CASE 0: convertToETH = false + // // CASE 1: convertToETH = true + // for (uint256 i = 0; i < 2; i++) { + // bool convertToETH = i > 0; + + // address friend = friends[i]; + // (,,, address creditAccount) = _openCreditAccount(); + + // CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); + + // tokenTestSuite.mint(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); + + // cmi.safeTokenTransfer( + // creditAccount, tokenTestSuite.addressOf(Tokens.WETH), friend, WETH_TRANSFER, convertToETH + // ); + + // expectBalance(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT - WETH_TRANSFER); + + // if (convertToETH) { + // assertEq(wethGateway.balanceOf(friend), WETH_TRANSFER, "Incorrect amount were sent to friend address"); + // } else { + // expectBalance(Tokens.WETH, friend, WETH_TRANSFER); + // } + + // // creditManager.closeCreditAccount( + // // creditAccount, ClosureAction.LIQUIDATE_ACCOUNT, 0, LIQUIDATOR, friend, 1, 0, DAI_ACCOUNT_AMOUNT, false + // // ); + // } + // } + + // + // SET PARAMS + // + + // /// @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 + + // function test_I_CM_65_closeCreditAccount_reverts_when_paused_and_liquidator_tries_to_close() public { + // 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); + // } + + /// @dev I:[CM-66]: calcNewCumulativeIndex works correctly for various values + function test_I_CM_66_calcNewCumulativeIndex_is_correct( + uint128 borrowedAmount, + uint256 indexAtOpen, + uint256 indexNow, + uint128 delta, + bool isIncrease + ) public { + // vm.assume(borrowedAmount > 100); + // vm.assume(uint256(borrowedAmount) + uint256(delta) <= 2 ** 128 - 1); + + // indexNow = indexNow < RAY ? indexNow + RAY : indexNow; + // indexAtOpen = indexAtOpen < RAY ? indexAtOpen + RAY : indexNow; + + // vm.assume(indexNow <= 100 * RAY); + // vm.assume(indexNow >= indexAtOpen); + // vm.assume(indexNow - indexAtOpen < 10 * RAY); + + // uint256 interest = uint256((borrowedAmount * indexNow) / indexAtOpen - borrowedAmount); + + // vm.assume(interest > 1); + + // if (!isIncrease && (delta > interest)) delta %= uint128(interest); + + // CreditManagerTestInternal cmi = new CreditManagerTestInternal( + // creditManager.poolService(), address(withdrawalManager) + // ); + + // if (isIncrease) { + // uint256 newIndex = CreditLogic.calcNewCumulativeIndex(borrowedAmount, delta, indexNow, indexAtOpen, true); + + // uint256 newInterestError = ((borrowedAmount + delta) * indexNow) / newIndex - (borrowedAmount + delta) + // - ((borrowedAmount * indexNow) / indexAtOpen - borrowedAmount); + + // uint256 newTotalDebt = ((borrowedAmount + delta) * indexNow) / newIndex; + + // assertLe((RAY * newInterestError) / newTotalDebt, 10000, "Interest error is larger than 10 ** -23"); + // } else { + // uint256 newIndex = cmi.calcNewCumulativeIndex(borrowedAmount, delta, indexNow, indexAtOpen, false); + + // uint256 newTotalDebt = ((borrowedAmount * indexNow) / newIndex); + // uint256 newInterestError = newTotalDebt - borrowedAmount - (interest - delta); + + // emit log_uint(indexNow); + // emit log_uint(indexAtOpen); + // emit log_uint(interest); + // emit log_uint(delta); + // emit log_uint(interest - delta); + // emit log_uint(newTotalDebt); + // emit log_uint(borrowedAmount); + // emit log_uint(newInterestError); + + // assertLe((RAY * newInterestError) / newTotalDebt, 10000, "Interest error is larger than 10 ** -23"); + // } + } + + // /// @dev I:[CM-67]: checkEmergencyPausable returns pause state and enable emergencyLiquidation if needed + // function test_I_CM_67_checkEmergencyPausable_returns_pause_state_and_enable_emergencyLiquidation_if_needed() public { + // bool p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, true); + // assertTrue(!p, "Incorrect paused() value for non-paused state"); + // assertTrue(!creditManager.emergencyLiquidation(), "Emergency liquidation true when expected false"); + + // vm.prank(CONFIGURATOR); + // creditManager.pause(); + + // p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, true); + // assertTrue(p, "Incorrect paused() value for paused state"); + // assertTrue(!creditManager.emergencyLiquidation(), "Emergency liquidation true when expected false"); + + // vm.prank(CONFIGURATOR); + // creditManager.unpause(); + + // vm.prank(CONFIGURATOR); + // creditManager.addEmergencyLiquidator(DUMB_ADDRESS); + // p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, true); + // assertTrue(!p, "Incorrect paused() value for non-paused state"); + // assertTrue(!creditManager.emergencyLiquidation(), "Emergency liquidation true when expected false"); + + // vm.prank(CONFIGURATOR); + // creditManager.pause(); + + // p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, true); + // assertTrue(p, "Incorrect paused() value for paused state"); + // assertTrue(creditManager.emergencyLiquidation(), "Emergency liquidation flase when expected true"); + + // p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, false); + // assertTrue(p, "Incorrect paused() value for paused state"); + // assertTrue(!creditManager.emergencyLiquidation(), "Emergency liquidation true when expected false"); + // } + + /// @dev I:[CM-68]: fullCollateralCheck checks tokens in correct order + function test_I_CM_68_fullCollateralCheck_is_evaluated_in_order_of_hints() public { + (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow, address creditAccount) = + _openCreditAccount(); + + uint256 daiBalance = tokenTestSuite.balanceOf(Tokens.DAI, creditAccount); + + tokenTestSuite.burn(Tokens.DAI, creditAccount, daiBalance); + + uint256 borrowAmountWithInterest = borrowedAmount * cumulativeIndexNow / cumulativeIndexLastUpdate; + uint256 interestAccured = borrowAmountWithInterest - borrowedAmount; + + (uint256 feeInterest,,,,) = creditManager.fees(); + + uint256 amountToRepay = ( + ((borrowAmountWithInterest + interestAccured * feeInterest / PERCENTAGE_FACTOR) * (10 ** 8)) + * PERCENTAGE_FACTOR / tokenTestSuite.prices(Tokens.DAI) + / creditManager.liquidationThresholds(tokenTestSuite.addressOf(Tokens.DAI)) + ) + WAD; + + tokenTestSuite.mint(Tokens.DAI, creditAccount, amountToRepay); + + tokenTestSuite.mint(Tokens.USDC, creditAccount, USDC_ACCOUNT_AMOUNT); + tokenTestSuite.mint(Tokens.USDT, creditAccount, 10); + tokenTestSuite.mint(Tokens.LINK, creditAccount, 10); + + // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.USDC)); + // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.USDT)); + // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.LINK)); + + uint256[] memory collateralHints = new uint256[](2); + collateralHints[0] = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDT)); + collateralHints[1] = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); + + vm.expectCall(tokenTestSuite.addressOf(Tokens.USDT), abi.encodeCall(IERC20.balanceOf, (creditAccount))); + vm.expectCall(tokenTestSuite.addressOf(Tokens.LINK), abi.encodeCall(IERC20.balanceOf, (creditAccount))); + + uint256 enabledTokensMap = 1 | creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)) + | creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDT)) + | creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); + + creditManager.fullCollateralCheck(creditAccount, enabledTokensMap, collateralHints, PERCENTAGE_FACTOR); + + // assertEq(cmi.fullCheckOrder(0), tokenTestSuite.addressOf(Tokens.USDT), "Token order incorrect"); + + // assertEq(cmi.fullCheckOrder(1), tokenTestSuite.addressOf(Tokens.LINK), "Token order incorrect"); + + // assertEq(cmi.fullCheckOrder(2), tokenTestSuite.addressOf(Tokens.DAI), "Token order incorrect"); + + // assertEq(cmi.fullCheckOrder(3), tokenTestSuite.addressOf(Tokens.USDC), "Token order incorrect"); + } + + /// @dev I:[CM-70]: fullCollateralCheck reverts when an illegal mask is passed in collateralHints + function test_I_CM_70_fullCollateralCheck_reverts_for_illegal_mask_in_hints() public { + (,,, address creditAccount) = _openCreditAccount(); + + vm.expectRevert(TokenNotAllowedException.selector); + + uint256[] memory ch = new uint256[](1); + ch[0] = 3; + + uint256 enabledTokensMap = 1; + + creditManager.fullCollateralCheck(creditAccount, enabledTokensMap, ch, PERCENTAGE_FACTOR); + } + + // /// @dev I:[CM-71]: rampLiquidationThreshold correctly updates the internal struct + // function test_I_CM_71_rampLiquidationThreshold_correctly_updates_parameters() public { + // _connectCreditManagerSuite(Tokens.DAI, true); + + // address usdc = tokenTestSuite.addressOf(Tokens.USDC); + + // CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); + + // vm.prank(CONFIGURATOR); + // cmi.setCollateralTokenData(usdc, 8500, 9000, uint40(block.timestamp), 3600 * 24 * 7); + + // CollateralTokenData memory cd = cmi.collateralTokensDataExt(cmi.getTokenMaskOrRevert(usdc)); + + // assertEq(uint256(cd.ltInitial), creditConfig.lt(Tokens.USDC), "Incorrect initial LT"); + + // assertEq(uint256(cd.ltFinal), 8500, "Incorrect final LT"); + + // assertEq(uint256(cd.timestampRampStart), block.timestamp, "Incorrect timestamp start"); + + // assertEq(uint256(cd.rampDuration), 3600 * 24 * 7, "Incorrect ramp duration"); + // } + + /// @dev I:[CM-72]: Ramping liquidation threshold fuzzing + function test_I_CM_72_liquidation_ramping_fuzzing( + uint16 initialLT, + uint16 newLT, + uint24 duration, + uint256 timestampCheck + ) public { + // initialLT = 1000 + (initialLT % (DEFAULT_UNDERLYING_LT - 999)); + // newLT = 1000 + (newLT % (DEFAULT_UNDERLYING_LT - 999)); + // duration = 3600 + (duration % (3600 * 24 * 90 - 3600)); + + // timestampCheck = block.timestamp + (timestampCheck % (duration + 1)); + + // address usdc = tokenTestSuite.addressOf(Tokens.USDC); + + // uint256 timestampStart = block.timestamp; + + // vm.startPrank(CONFIGURATOR); + // creditManager.setCollateralTokenData(usdc, initialLT); + // creditManager.rampLiquidationThreshold(usdc, newLT, uint40(block.timestamp), duration); + + // assertEq(creditManager.liquidationThresholds(usdc), initialLT, "LT at ramping start incorrect"); + + // uint16 expectedLT; + // if (newLT >= initialLT) { + // expectedLT = uint16( + // uint256(initialLT) + // + (uint256(newLT - initialLT) * (timestampCheck - timestampStart)) / uint256(duration) + // ); + // } else { + // expectedLT = uint16( + // uint256(initialLT) + // - (uint256(initialLT - newLT) * (timestampCheck - timestampStart)) / uint256(duration) + // ); + // } + + // vm.warp(timestampCheck); + // uint16 actualLT = creditManager.liquidationThresholds(usdc); + // uint16 diff = actualLT > expectedLT ? actualLT - expectedLT : expectedLT - actualLT; + + // assertLe(diff, 1, "LT off by more than 1"); + + // vm.warp(timestampStart + duration + 1); + + // assertEq(creditManager.liquidationThresholds(usdc), newLT, "LT at ramping end incorrect"); + } +} diff --git a/contracts/test/integration/credit/CreditManager.t.sol b/contracts/test/integration/credit/CreditManager.t.sol deleted file mode 100644 index 19caca46..00000000 --- a/contracts/test/integration/credit/CreditManager.t.sol +++ /dev/null @@ -1,2444 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; - -import {IAddressProvider} from "@gearbox-protocol/core-v2/contracts/interfaces/IAddressProvider.sol"; -import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; - -import {AccountFactory} from "@gearbox-protocol/core-v2/contracts/core/AccountFactory.sol"; -import {ICreditAccount} from "@gearbox-protocol/core-v2/contracts/interfaces/ICreditAccount.sol"; -import { - ICreditManagerV3, - ICreditManagerV3Events, - ClosureAction, - CollateralTokenData, - ManageDebtAction -} from "../../../interfaces/ICreditManagerV3.sol"; - -import {IPriceOracleV2, IPriceOracleV2Ext} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; -import {IWETHGateway} from "../../../interfaces/IWETHGateway.sol"; -import {IWithdrawalManager} from "../../../interfaces/IWithdrawalManager.sol"; - -import {CreditManagerV3} from "../../../credit/CreditManagerV3.sol"; - -import {IPoolService} from "@gearbox-protocol/core-v2/contracts/interfaces/IPoolService.sol"; - -import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ERC20Mock} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/ERC20Mock.sol"; -import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; - -// LIBS & TRAITS -import {BitMask} from "../../../libraries/BitMask.sol"; -// TESTS - -import "../../lib/constants.sol"; -import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; - -// EXCEPTIONS -import {TokenAlreadyAddedException} from "../../../interfaces/IExceptions.sol"; - -// MOCKS -import {PriceFeedMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/oracles/PriceFeedMock.sol"; -import {PoolServiceMock} from "../../mocks/pool/PoolServiceMock.sol"; -import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/adapters/TargetContractMock.sol"; -import { - ERC20ApproveRestrictedRevert, - ERC20ApproveRestrictedFalse -} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/ERC20ApproveRestricted.sol"; - -// SUITES -import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; -import {Tokens} from "../../config/Tokens.sol"; -import {CreditManagerTestSuite} from "../../suites/CreditManagerTestSuite.sol"; - -import {CreditManagerTestInternal} from "../../mocks/credit/CreditManagerTestInternal.sol"; - -import {CreditConfig} from "../../config/CreditConfig.sol"; - -// EXCEPTIONS -import "../../../interfaces/IExceptions.sol"; - -import {Test} from "forge-std/Test.sol"; -import "forge-std/console.sol"; - -/// @title AddressRepository -/// @notice Stores addresses of deployed contracts -contract CreditManagerTest is Test, ICreditManagerV3Events, BalanceHelper { - using BitMask for uint256; - - CreditManagerTestSuite cms; - - IAddressProvider addressProvider; - IWETH wethToken; - - AccountFactory af; - CreditManagerV3 creditManager; - PoolServiceMock poolMock; - IPriceOracleV2 priceOracle; - IWETHGateway wethGateway; - IWithdrawalManager withdrawalManager; - ACL acl; - address underlying; - - CreditConfig creditConfig; - - function setUp() public { - tokenTestSuite = new TokensTestSuite(); - - tokenTestSuite.topUpWETH{value: 100 * WAD}(); - _connectCreditManagerSuite(Tokens.DAI, false); - } - - /// - /// HELPERS - - function _connectCreditManagerSuite(Tokens t, bool internalSuite) internal { - creditConfig = new CreditConfig(tokenTestSuite, t); - cms = new CreditManagerTestSuite(creditConfig, internalSuite, false, 1); - - acl = cms.acl(); - - addressProvider = cms.addressProvider(); - af = cms.af(); - - poolMock = cms.poolMock(); - withdrawalManager = cms.withdrawalManager(); - - creditManager = cms.creditManager(); - - priceOracle = creditManager.priceOracle(); - underlying = creditManager.underlying(); - wethGateway = IWETHGateway(creditManager.wethGateway()); - } - - /// @dev Opens credit account for testing management functions - function _openCreditAccount() - internal - returns ( - uint256 borrowedAmount, - uint256 cumulativeIndexLastUpdate, - uint256 cumulativeIndexAtClose, - address creditAccount - ) - { - return cms.openCreditAccount(); - } - - function expectTokenIsEnabled(address creditAccount, Tokens t, bool expectedState) internal { - bool state = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(t)) - & creditManager.enabledTokensMaskOf(creditAccount) != 0; - assertTrue( - state == expectedState, - string( - abi.encodePacked( - "Token ", - tokenTestSuite.symbols(t), - state ? " enabled as not expected" : " not enabled as expected " - ) - ) - ); - } - - function mintBalance(address creditAccount, Tokens t, uint256 amount, bool enable) internal { - tokenTestSuite.mint(t, creditAccount, amount); - // if (enable) { - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(t)); - // } - } - - function _addAndEnableTokens(address creditAccount, uint256 numTokens, uint256 balance) internal { - for (uint256 i = 0; i < numTokens; i++) { - ERC20Mock t = new ERC20Mock("new token", "nt", 18); - PriceFeedMock pf = new PriceFeedMock(10**8, 8); - - vm.startPrank(CONFIGURATOR); - creditManager.addToken(address(t)); - IPriceOracleV2Ext(address(priceOracle)).addPriceFeed(address(t), address(pf)); - creditManager.setCollateralTokenData(address(t), 8000, 8000, type(uint40).max, 0); - vm.stopPrank(); - - t.mint(creditAccount, balance); - - // creditManager.checkAndEnableToken(address(t)); - } - } - - function _getRandomBits(uint256 ones, uint256 zeros, uint256 randomValue) - internal - pure - returns (bool[] memory result, uint256 breakPoint) - { - if ((ones + zeros) == 0) { - result = new bool[](0); - breakPoint = 0; - return (result, breakPoint); - } - - uint256 onesCurrent = ones; - uint256 zerosCurrent = zeros; - - result = new bool[](ones + zeros); - uint256 i = 0; - - while (onesCurrent + zerosCurrent > 0) { - uint256 rand = uint256(keccak256(abi.encodePacked(randomValue))) % (onesCurrent + zerosCurrent); - if (rand < onesCurrent) { - result[i] = true; - onesCurrent--; - } else { - result[i] = false; - zerosCurrent--; - } - - i++; - } - - if (ones > 0) { - uint256 breakpointCounter = (uint256(keccak256(abi.encodePacked(randomValue))) % (ones)) + 1; - - for (uint256 j = 0; j < result.length; j++) { - if (result[j]) { - breakpointCounter--; - } - - if (breakpointCounter == 0) { - breakPoint = j; - break; - } - } - } - } - - function enableTokensMoreThanLimit(address creditAccount) internal { - uint256 maxAllowedEnabledTokenLength = creditManager.maxAllowedEnabledTokenLength(); - _addAndEnableTokens(creditAccount, maxAllowedEnabledTokenLength, 2); - } - - function _openAccountAndTransferToCF() internal returns (address creditAccount) { - (,,, creditAccount) = _openCreditAccount(); - creditManager.transferAccountOwnership(creditAccount, address(this)); - } - - function _baseFullCollateralCheck(address creditAccount) internal { - // TODO: CHANGE - creditManager.fullCollateralCheck(creditAccount, 0, new uint256[](0), 10000); - } - - /// - /// - /// TESTS - /// - /// - /// @dev [CM-1]: credit manager reverts if were called non-creditFacade - function test_CM_01_constructor_sets_correct_values() public { - creditManager = new CreditManagerV3(address(poolMock), address(withdrawalManager)); - - assertEq(address(creditManager.poolService()), address(poolMock), "Incorrect poolSerivice"); - - assertEq(address(creditManager.pool()), address(poolMock), "Incorrect pool"); - - assertEq(creditManager.underlying(), tokenTestSuite.addressOf(Tokens.DAI), "Incorrect underlying"); - - (address token, uint16 lt) = creditManager.collateralTokens(0); - - assertEq(token, tokenTestSuite.addressOf(Tokens.DAI), "Incorrect underlying"); - - assertEq( - creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)), - 1, - "Incorrect token mask for underlying token" - ); - - assertEq(lt, 0, "Incorrect LT for underlying"); - - assertEq(creditManager.wethAddress(), addressProvider.getWethToken(), "Incorrect WETH token"); - - assertEq(address(creditManager.wethGateway()), addressProvider.getWETHGateway(), "Incorrect WETH Gateway"); - - assertEq(address(creditManager.priceOracle()), addressProvider.getPriceOracle(), "Incorrect Price oracle"); - - assertEq(address(creditManager.creditConfigurator()), address(this), "Incorrect creditConfigurator"); - } - - /// @dev [CM-2]:credit account management functions revert if were called non-creditFacade - /// Functions list: - /// - openCreditAccount - /// - closeCreditAccount - /// - manadgeDebt - /// - addCollateral - /// - transferOwnership - /// All these functions have creditFacadeOnly modifier - function test_CM_02_credit_account_management_functions_revert_if_not_called_by_creditFacadeCall() public { - assertEq(creditManager.creditFacade(), address(this)); - - vm.startPrank(USER); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.openCreditAccount(200000, address(this), false); - - // vm.expectRevert(CallerNotCreditFacadeException.selector); - // creditManager.closeCreditAccount( - // DUMB_ADDRESS, ClosureAction.LIQUIDATE_ACCOUNT, 0, DUMB_ADDRESS, DUMB_ADDRESS, type(uint256).max, false - // ); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.manageDebt(DUMB_ADDRESS, 100, 0, ManageDebtAction.INCREASE_DEBT); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.addCollateral(DUMB_ADDRESS, DUMB_ADDRESS, DUMB_ADDRESS, 100); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.transferAccountOwnership(DUMB_ADDRESS, DUMB_ADDRESS); - - vm.stopPrank(); - } - - /// @dev [CM-3]:credit account execution functions revert if were called non-creditFacade & non-adapters - /// Functions list: - /// - approveCreditAccount - /// - executeOrder - /// - checkAndEnableToken - /// - fullCollateralCheck - /// - disableToken - /// - changeEnabledTokens - function test_CM_03_credit_account_execution_functions_revert_if_not_called_by_creditFacade_or_adapters() public { - assertEq(creditManager.creditFacade(), address(this)); - - vm.startPrank(USER); - - vm.expectRevert(CallerNotAdapterException.selector); - creditManager.approveCreditAccount(DUMB_ADDRESS, 100); - - vm.expectRevert(CallerNotAdapterException.selector); - creditManager.executeOrder(bytes("0")); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.fullCollateralCheck(DUMB_ADDRESS, 0, new uint256[](0), 10000); - - vm.stopPrank(); - } - - /// @dev [CM-4]:credit account configuration functions revert if were called non-configurator - /// Functions list: - /// - addToken - /// - setParams - /// - setLiquidationThreshold - /// - setForbidMask - /// - setContractAllowance - /// - upgradeContracts - /// - setCreditConfigurator - /// - addEmergencyLiquidator - /// - removeEmergenceLiquidator - function test_CM_04_credit_account_configurator_functions_revert_if_not_called_by_creditConfigurator() public { - assertEq(creditManager.creditFacade(), address(this)); - - vm.startPrank(USER); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.addToken(DUMB_ADDRESS); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setParams(0, 0, 0, 0, 0); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setCollateralTokenData(DUMB_ADDRESS, 0, 0, 0, 0); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setContractAllowance(DUMB_ADDRESS, DUMB_ADDRESS); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setCreditFacade(DUMB_ADDRESS); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setPriceOracle(DUMB_ADDRESS); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setCreditConfigurator(DUMB_ADDRESS); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setMaxEnabledTokens(255); - - vm.stopPrank(); - } - - // TODO: REMOVE OUTDATED - // /// @dev [CM-5]:credit account management+execution functions revert if were called non-creditFacade - // /// Functions list: - // /// - openCreditAccount - // /// - closeCreditAccount - // /// - manadgeDebt - // /// - addCollateral - // /// - transferOwnership - // /// All these functions have whenNotPaused modifier - // function test_CM_05_pause_pauses_management_functions() public { - // address root = acl.owner(); - // vm.prank(root); - - // acl.addPausableAdmin(root); - - // vm.prank(root); - // creditManager.pause(); - - // assertEq(creditManager.creditFacade(), address(this)); - - // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // creditManager.openCreditAccount(200000, address(this)); - - // // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // // creditManager.closeCreditAccount( - // // DUMB_ADDRESS, ClosureAction.LIQUIDATE_ACCOUNT, 0, DUMB_ADDRESS, DUMB_ADDRESS, type(uint256).max, false - // // ); - - // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // creditManager.manageDebt(DUMB_ADDRESS, 100, ManageDebtAction.INCREASE_DEBT); - - // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // creditManager.addCollateral(DUMB_ADDRESS, DUMB_ADDRESS, DUMB_ADDRESS, 100); - - // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // creditManager.transferAccountOwnership(DUMB_ADDRESS, DUMB_ADDRESS); - - // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // creditManager.approveCreditAccount(DUMB_ADDRESS, DUMB_ADDRESS, 100); - - // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // creditManager.executeOrder(DUMB_ADDRESS, bytes("dd")); - // } - - // - // REVERTS IF CREDIT ACCOUNT NOT EXISTS - // - - /// @dev [CM-6A]: management function reverts if account not exists - /// Functions list: - /// - getCreditAccountOrRevert - /// - closeCreditAccount - /// - transferOwnership - - function test_CM_06A_management_functions_revert_if_account_does_not_exist() public { - // vm.expectRevert(CreditAccountNotExistsException.selector); - // creditManager.getCreditAccountOrRevert(USER); - - // vm.expectRevert(CreditAccountNotExistsException.selector); - // creditManager.closeCreditAccount( - // USER, ClosureAction.LIQUIDATE_ACCOUNT, 0, DUMB_ADDRESS, DUMB_ADDRESS, type(uint256).max, false - // ); - - vm.expectRevert(CreditAccountNotExistsException.selector); - creditManager.transferAccountOwnership(USER, DUMB_ADDRESS); - } - - /// @dev [CM-6A]: external call functions revert when the Credit Facade has no account - /// Functions list: - /// - executeOrder - /// - approveCreditAccount - function test_CM_06B_extenrnal_ca_only_functions_revert_when_ec_is_not_set() public { - address token = tokenTestSuite.addressOf(Tokens.DAI); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - vm.prank(ADAPTER); - vm.expectRevert(ExternalCallCreditAccountNotSetException.selector); - creditManager.approveCreditAccount(token, 100); - - // / TODO: decide about test - vm.prank(ADAPTER); - vm.expectRevert(ExternalCallCreditAccountNotSetException.selector); - creditManager.executeOrder(bytes("dd")); - } - - /// - /// OPEN CREDIT ACCOUNT - /// - - /// @dev [CM-7]: openCreditAccount reverts if zero address or address exists - function test_CM_07_openCreditAccount_reverts_if_address_exists() public { - // // Existing address case - // creditManager.openCreditAccount(1, USER); - // vm.expectRevert(UserAlreadyHasAccountException.selector); - // creditManager.openCreditAccount(1, USER); - } - - /// @dev [CM-8]: openCreditAccount sets correct values and transfers tokens from pool - function test_CM_08_openCreditAccount_sets_correct_values_and_transfers_tokens_from_pool() public { - address expectedCreditAccount = AccountFactory(addressProvider.getAccountFactory()).head(); - - uint256 blockAtOpen = block.number; - uint256 cumulativeAtOpen = 1012; - poolMock.setCumulative_RAY(cumulativeAtOpen); - - // Existing address case - address creditAccount = creditManager.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, false); - assertEq(creditAccount, expectedCreditAccount, "Incorrecct credit account address"); - - (uint256 debt, uint256 cumulativeIndexLastUpdate,,,,) = creditManager.creditAccountInfo(creditAccount); - - assertEq(debt, DAI_ACCOUNT_AMOUNT, "Incorrect borrowed amount set in CA"); - assertEq(cumulativeIndexLastUpdate, cumulativeAtOpen, "Incorrect cumulativeIndexLastUpdate set in CA"); - - assertEq(ICreditAccount(creditAccount).since(), blockAtOpen, "Incorrect since set in CA"); - - expectBalance(Tokens.DAI, creditAccount, DAI_ACCOUNT_AMOUNT); - assertEq(poolMock.lendAmount(), DAI_ACCOUNT_AMOUNT, "Incorrect DAI_ACCOUNT_AMOUNT in Pool call"); - assertEq(poolMock.lendAccount(), creditAccount, "Incorrect credit account in lendCreditAccount call"); - // assertEq(creditManager.creditAccounts(USER), creditAccount, "Credit account is not associated with user"); - assertEq(creditManager.enabledTokensMaskOf(creditAccount), 0, "Incorrect enabled token mask"); - } - - // - // CLOSE CREDIT ACCOUNT - // - - /// @dev [CM-9]: closeCreditAccount returns credit account to factory and - /// remove borrower from creditAccounts mapping - function test_CM_09_close_credit_account_returns_credit_account_and_remove_borrower_from_map() public { - (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); - - assertTrue( - creditAccount != AccountFactory(addressProvider.getAccountFactory()).tail(), - "credit account is already in tail!" - ); - - // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount - tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); - - // Increase block number cause it's forbidden to close credit account in the same block - vm.roll(block.number + 1); - - // creditManager.closeCreditAccount( - // creditAccount, ClosureAction.CLOSE_ACCOUNT, 0, USER, USER, 0, 0, DAI_ACCOUNT_AMOUNT, false - // ); - - assertEq( - creditAccount, - AccountFactory(addressProvider.getAccountFactory()).tail(), - "credit account is not in accountFactory tail!" - ); - - // vm.expectRevert(CreditAccountNotExistsException.selector); - // creditManager.getCreditAccountOrRevert(USER); - } - - /// @dev [CM-10]: 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_CM_10_close_credit_account_returns_underlying_token_if_not_liquidated() public { - ( - uint256 borrowedAmount, - uint256 cumulativeIndexLastUpdate, - uint256 cumulativeIndexAtClose, - address creditAccount - ) = _openCreditAccount(); - - uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(poolMock)); - - // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount - tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); - - uint256 interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; - - (uint16 feeInterest,,,,) = creditManager.fees(); - - uint256 profit = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - - uint256 amountToPool = borrowedAmount + interestAccrued + profit; - - vm.expectCall( - address(poolMock), - abi.encodeWithSelector(IPoolService.repayCreditAccount.selector, borrowedAmount, profit, 0) - ); - - // (uint256 remainingFunds, uint256 loss) = creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.CLOSE_ACCOUNT, - // 0, - // USER, - // FRIEND, - // 1, - // 0, - // DAI_ACCOUNT_AMOUNT + interestAccrued, - // false - // ); - - // assertEq(remainingFunds, 0, "Remaining funds is not zero!"); - - // assertEq(loss, 0, "Loss is not zero"); - - expectBalance(Tokens.DAI, creditAccount, 1); - - expectBalance(Tokens.DAI, address(poolMock), poolBalanceBefore + amountToPool); - - expectBalance(Tokens.DAI, FRIEND, 2 * borrowedAmount - amountToPool - 1, "Incorrect amount were paid back"); - } - - /// @dev [CM-11]: 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_CM_11_close_credit_account_charges_caller_if_underlying_token_not_enough() public { - ( - uint256 borrowedAmount, - uint256 cumulativeIndexLastUpdate, - uint256 cumulativeIndexAtClose, - address creditAccount - ) = _openCreditAccount(); - - uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(poolMock)); - - // Transfer funds to USER account to be able to cover extra cost - tokenTestSuite.mint(Tokens.DAI, USER, borrowedAmount); - - uint256 interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; - - (uint16 feeInterest,,,,) = creditManager.fees(); - - uint256 profit = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - - uint256 amountToPool = borrowedAmount + interestAccrued + profit; - - vm.expectCall( - address(poolMock), - abi.encodeWithSelector(IPoolService.repayCreditAccount.selector, borrowedAmount, profit, 0) - ); - - // (uint256 remainingFunds, uint256 loss) = creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.CLOSE_ACCOUNT, - // 0, - // USER, - // FRIEND, - // 1, - // 0, - // DAI_ACCOUNT_AMOUNT + interestAccrued, - // false - // ); - // assertEq(remainingFunds, 0, "Remaining funds is not zero!"); - - // assertEq(loss, 0, "Loss is not zero"); - - expectBalance(Tokens.DAI, creditAccount, 1, "Credit account balance != 1"); - - expectBalance(Tokens.DAI, address(poolMock), poolBalanceBefore + amountToPool); - - expectBalance(Tokens.DAI, USER, 2 * borrowedAmount - amountToPool - 1, "Incorrect amount were paid back"); - - expectBalance(Tokens.DAI, FRIEND, 0, "Incorrect amount were paid back"); - } - - /// @dev [CM-12]: closeCreditAccount 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_CM_12_close_credit_account_charges_caller_if_underlying_token_not_enough() public { - for (uint256 i = 0; i < 2; i++) { - uint256 friendBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, FRIEND); - - ClosureAction action = i == 1 ? ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT : ClosureAction.LIQUIDATE_ACCOUNT; - uint256 interestAccrued; - uint256 borrowedAmount; - address creditAccount; - - { - uint256 cumulativeIndexLastUpdate; - uint256 cumulativeIndexAtClose; - (borrowedAmount, cumulativeIndexLastUpdate, cumulativeIndexAtClose, creditAccount) = - _openCreditAccount(); - - interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; - } - - uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(poolMock)); - uint256 discount; - - { - (,, uint16 liquidationDiscount,, uint16 liquidationDiscountExpired) = creditManager.fees(); - discount = action == ClosureAction.LIQUIDATE_ACCOUNT ? liquidationDiscount : liquidationDiscountExpired; - } - - // uint256 totalValue = borrowedAmount; - uint256 amountToPool = (borrowedAmount * discount) / PERCENTAGE_FACTOR; - - { - uint256 loss = borrowedAmount + interestAccrued - amountToPool; - - vm.expectCall( - address(poolMock), - abi.encodeWithSelector(IPoolService.repayCreditAccount.selector, borrowedAmount, 0, loss) - ); - } - { - uint256 a = borrowedAmount + interestAccrued; - - // (uint256 remainingFunds,) = creditManager.closeCreditAccount( - // creditAccount, action, borrowedAmount, LIQUIDATOR, FRIEND, 1, 0, a, false - // ); - } - - expectBalance(Tokens.DAI, creditAccount, 1, "Credit account balance != 1"); - - expectBalance(Tokens.DAI, address(poolMock), poolBalanceBefore + amountToPool); - - expectBalance( - Tokens.DAI, - FRIEND, - friendBalanceBefore + (borrowedAmount * (PERCENTAGE_FACTOR - discount)) / PERCENTAGE_FACTOR - - (i == 2 ? 0 : 1), - "Incorrect amount were paid to liqiudator friend address" - ); - } - } - - /// @dev [CM-13]: openCreditAccount 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_CM_13_close_credit_account_charges_caller_if_underlying_token_not_enough() public { - for (uint256 i = 0; i < 2; i++) { - setUp(); - uint256 borrowedAmount; - address creditAccount; - - uint256 expectedRemainingFunds = 100 * WAD; - - uint256 profit; - uint256 amountToPool; - uint256 totalValue; - uint256 interestAccrued; - { - uint256 cumulativeIndexLastUpdate; - uint256 cumulativeIndexAtClose; - (borrowedAmount, cumulativeIndexLastUpdate, cumulativeIndexAtClose, creditAccount) = - _openCreditAccount(); - - interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; - - uint16 feeInterest; - uint16 feeLiquidation; - uint16 liquidationDiscount; - - { - (feeInterest,,,,) = creditManager.fees(); - } - - { - uint16 feeLiquidationNormal; - uint16 feeLiquidationExpired; - - (, feeLiquidationNormal,, feeLiquidationExpired,) = creditManager.fees(); - - feeLiquidation = (i == 0 || i == 2) ? feeLiquidationNormal : feeLiquidationExpired; - } - - { - uint16 liquidationDiscountNormal; - uint16 liquidationDiscountExpired; - - (feeInterest,, liquidationDiscountNormal,, liquidationDiscountExpired) = creditManager.fees(); - - liquidationDiscount = i == 1 ? liquidationDiscountExpired : liquidationDiscountNormal; - } - - uint256 profitInterest = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - - amountToPool = borrowedAmount + 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(poolMock)); - - 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, borrowedAmount, "creditAccount has incorrect initial balance"); - - vm.expectCall( - address(poolMock), - abi.encodeWithSelector(IPoolService.repayCreditAccount.selector, borrowedAmount, profit, 0) - ); - - uint256 remainingFunds; - - { - uint256 loss; - - uint256 a = borrowedAmount + interestAccrued; - // (remainingFunds, loss) = creditManager.closeCreditAccount( - // creditAccount, - // i == 1 ? ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT : ClosureAction.LIQUIDATE_ACCOUNT, - // totalValue, - // LIQUIDATOR, - // FRIEND, - // 1, - // 0, - // a, - // 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(poolMock), poolBalanceBefore + amountToPool, "INCORRECT POOL BALANCE"); - } - - expectBalance( - Tokens.DAI, - LIQUIDATOR, - totalValue + borrowedAmount - amountToPool - remainingFunds - 1, - "Incorrect amount were paid to lqiudaidator" - ); - } - } - - /// @dev [CM-14]: 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_CM_14_close_credit_account_with_nonzero_skipTokenMask_sends_correct_tokens() public { - (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); - creditManager.transferAccountOwnership(creditAccount, address(this)); - - tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); - tokenTestSuite.mint(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.WETH)); - - tokenTestSuite.mint(Tokens.USDC, creditAccount, USDC_EXCHANGE_AMOUNT); - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.USDC)); - - tokenTestSuite.mint(Tokens.LINK, creditAccount, LINK_EXCHANGE_AMOUNT); - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.LINK)); - - uint256 wethTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - uint256 usdcTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)); - uint256 linkTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - - creditManager.transferAccountOwnership(creditAccount, USER); - - // creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.CLOSE_ACCOUNT, - // 0, - // USER, - // FRIEND, - // wethTokenMask | usdcTokenMask | linkTokenMask, - // wethTokenMask | usdcTokenMask, - // DAI_ACCOUNT_AMOUNT, - // 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 [CM-16]: closeCreditAccount sends ETH for WETH creditManger to borrower - /// CASE: CLOSURE - /// Underlying token: WETH - function test_CM_16_close_weth_credit_account_sends_eth_to_borrower() public { - // It takes "clean" address which doesn't holds any assets - - _connectCreditManagerSuite(Tokens.WETH, false); - - /// CLOSURE CASE - ( - uint256 borrowedAmount, - uint256 cumulativeIndexLastUpdate, - uint256 cumulativeIndexAtClose, - address creditAccount - ) = _openCreditAccount(); - - // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount - tokenTestSuite.mint(Tokens.WETH, creditAccount, borrowedAmount); - - uint256 interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; - - // creditManager.closeCreditAccount(USER, ClosureAction.CLOSE_ACCOUNT, 0, USER, USER, 0, true); - - // creditManager.closeCreditAccount( - // creditAccount, ClosureAction.CLOSE_ACCOUNT, 0, USER, USER, 1, 0, borrowedAmount + interestAccrued, true - // ); - - expectBalance(Tokens.WETH, creditAccount, 1); - - (uint16 feeInterest,,,,) = creditManager.fees(); - - uint256 profit = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - - uint256 amountToPool = borrowedAmount + interestAccrued + profit; - - assertEq( - wethGateway.balanceOf(USER), - 2 * borrowedAmount - amountToPool - 1, - "Incorrect amount deposited on wethGateway" - ); - } - - /// @dev [CM-17]: closeCreditAccount sends ETH for WETH creditManger to borrower - /// CASE: CLOSURE - /// Underlying token: DAI - function test_CM_17_close_dai_credit_account_sends_eth_to_borrower() public { - /// CLOSURE CASE - (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); - creditManager.transferAccountOwnership(creditAccount, address(this)); - - // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount - tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); - - // 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)); - - creditManager.transferAccountOwnership(creditAccount, USER); - // creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.CLOSE_ACCOUNT, - // 0, - // USER, - // USER, - // wethTokenMask | daiTokenMask, - // 0, - // borrowedAmount, - // true - // ); - - expectBalance(Tokens.WETH, creditAccount, 1); - - assertEq(wethGateway.balanceOf(USER), WETH_EXCHANGE_AMOUNT - 1, "Incorrect amount deposited on wethGateway"); - } - - /// @dev [CM-18]: closeCreditAccount sends ETH for WETH creditManger to borrower - /// CASE: LIQUIDATION - function test_CM_18_close_credit_account_sends_eth_to_liquidator_and_weth_to_borrower() public { - /// 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, false); - - /// CLOSURE CASE - (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); - - // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount - tokenTestSuite.mint(Tokens.WETH, creditAccount, borrowedAmount); - - uint256 totalValue = borrowedAmount * 2; - - uint256 wethTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - uint256 daiTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)); - - // (uint256 remainingFunds,) = creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.LIQUIDATE_ACCOUNT, - // totalValue, - // LIQUIDATOR, - // FRIEND, - // wethTokenMask | daiTokenMask, - // 0, - // borrowedAmount, - // 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( - wethGateway.balanceOf(FRIEND), - (totalValue * (PERCENTAGE_FACTOR - liquidationDiscount)) / PERCENTAGE_FACTOR, - "Incorrect amount were paid to liqiudator friend address" - ); - } - - /// @dev [CM-19]: closeCreditAccount sends ETH for WETH creditManger to borrower - /// CASE: LIQUIDATION - /// Underlying token: DAI - function test_CM_19_close_dai_credit_account_sends_eth_to_liquidator() public { - /// CLOSURE CASE - (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); - creditManager.transferAccountOwnership(creditAccount, address(this)); - - // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount - tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); - - // 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)); - - // (uint256 remainingFunds,) = creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.LIQUIDATE_ACCOUNT, - // borrowedAmount, - // LIQUIDATOR, - // FRIEND, - // wethTokenMask | daiTokenMask, - // 0, - // borrowedAmount, - // true - // ); - - expectBalance(Tokens.WETH, creditAccount, 1); - - assertEq( - wethGateway.balanceOf(FRIEND), - WETH_EXCHANGE_AMOUNT - 1, - "Incorrect amount were paid to liqiudator friend address" - ); - } - - // - // MANAGE DEBT - // - - /// @dev [CM-20]: manageDebt correctly increases debt - function test_CM_20_manageDebt_correctly_increases_debt(uint128 amount) public { - (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate,, address creditAccount) = cms.openCreditAccount(1); - - tokenTestSuite.mint(Tokens.DAI, address(poolMock), amount); - - poolMock.setCumulative_RAY(cumulativeIndexLastUpdate * 2); - - uint256 expectedNewCulumativeIndex = - (2 * cumulativeIndexLastUpdate * (borrowedAmount + amount)) / (2 * borrowedAmount + amount); - - (uint256 newBorrowedAmount,,) = - creditManager.manageDebt(creditAccount, amount, 1, ManageDebtAction.INCREASE_DEBT); - - assertEq(newBorrowedAmount, borrowedAmount + amount, "Incorrect returned newBorrowedAmount"); - - // assertLe( - // (ICreditAccount(creditAccount).cumulativeIndexLastUpdate() * (10 ** 6)) / expectedNewCulumativeIndex, - // 10 ** 6, - // "Incorrect cumulative index" - // ); - - (uint256 debt,,,,,) = creditManager.creditAccountInfo(creditAccount); - assertEq(debt, newBorrowedAmount, "Incorrect borrowedAmount"); - - expectBalance(Tokens.DAI, creditAccount, newBorrowedAmount, "Incorrect balance on credit account"); - - assertEq(poolMock.lendAmount(), amount, "Incorrect lend amount"); - - assertEq(poolMock.lendAccount(), creditAccount, "Incorrect lend account"); - } - - /// @dev [CM-21]: manageDebt correctly decreases debt - function test_CM_21_manageDebt_correctly_decreases_debt(uint128 amount) public { - // tokenTestSuite.mint(Tokens.DAI, address(poolMock), (uint256(type(uint128).max) * 14) / 10); - - // (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow, address creditAccount) = - // cms.openCreditAccount((uint256(type(uint128).max) * 14) / 10); - - // (,, uint256 totalDebt) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // uint256 expectedInterestAndFees; - // uint256 expectedBorrowAmount; - // if (amount >= totalDebt - borrowedAmount) { - // expectedInterestAndFees = 0; - // expectedBorrowAmount = totalDebt - amount; - // } else { - // expectedInterestAndFees = totalDebt - borrowedAmount - amount; - // expectedBorrowAmount = borrowedAmount; - // } - - // (uint256 newBorrowedAmount,) = - // creditManager.manageDebt(creditAccount, amount, 1, ManageDebtAction.DECREASE_DEBT); - - // assertEq(newBorrowedAmount, expectedBorrowAmount, "Incorrect returned newBorrowedAmount"); - - // if (amount >= totalDebt - borrowedAmount) { - // (,, uint256 newTotalDebt) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // assertEq(newTotalDebt, newBorrowedAmount, "Incorrect new interest"); - // } else { - // (,, uint256 newTotalDebt) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // assertLt( - // (RAY * (newTotalDebt - newBorrowedAmount)) / expectedInterestAndFees - RAY, - // 10000, - // "Incorrect new interest" - // ); - // } - // uint256 cumulativeIndexLastUpdateAfter; - // { - // uint256 debt; - // (debt, cumulativeIndexLastUpdateAfter,,,,) = creditManager.creditAccountInfo(creditAccount); - - // assertEq(debt, newBorrowedAmount, "Incorrect borrowedAmount"); - // } - - // expectBalance(Tokens.DAI, creditAccount, borrowedAmount - amount, "Incorrect balance on credit account"); - - // if (amount >= totalDebt - borrowedAmount) { - // assertEq(cumulativeIndexLastUpdateAfter, cumulativeIndexNow, "Incorrect cumulativeIndexLastUpdate"); - // } else { - // CreditManagerTestInternal cmi = new CreditManagerTestInternal( - // creditManager.poolService(), address(withdrawalManager) - // ); - - // { - // (uint256 feeInterest,,,,) = creditManager.fees(); - // amount = uint128((uint256(amount) * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR + feeInterest)); - // } - - // assertEq( - // cumulativeIndexLastUpdateAfter, - // cmi.calcNewCumulativeIndex(borrowedAmount, amount, cumulativeIndexNow, cumulativeIndexLastUpdate, false), - // "Incorrect cumulativeIndexLastUpdate" - // ); - // } - } - - // - // ADD COLLATERAL - // - - /// @dev [CM-22]: add collateral transfers money and returns token mask - - function test_CM_22_add_collateral_transfers_money_and_returns_token_mask() public { - (,,, address creditAccount) = _openCreditAccount(); - - tokenTestSuite.mint(Tokens.WETH, FRIEND, WETH_EXCHANGE_AMOUNT); - tokenTestSuite.approve(Tokens.WETH, FRIEND, address(creditManager)); - - expectBalance(Tokens.WETH, creditAccount, 0, "Non-zero WETH balance"); - - expectTokenIsEnabled(creditAccount, Tokens.WETH, false); - - uint256 tokenMask = creditManager.addCollateral( - FRIEND, creditAccount, tokenTestSuite.addressOf(Tokens.WETH), WETH_EXCHANGE_AMOUNT - ); - - expectBalance(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT, "Non-zero WETH balance"); - - expectBalance(Tokens.WETH, FRIEND, 0, "Incorrect FRIEND balance"); - - assertEq( - tokenMask, - creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)), - "Incorrect return result" - ); - - // expectTokenIsEnabled(creditAccount, Tokens.WETH, true); - } - - // - // TRANSFER ACCOUNT OWNERSHIP - // - - /// @dev [CM-23]: transferAccountOwnership reverts if to equals 0 or creditAccount is linked with "to" address - - function test_CM_23_transferAccountOwnership_reverts_if_account_exists() public { - // _openCreditAccount(); - - // creditManager.openCreditAccount(1, FRIEND); - - // // Existing account case - // vm.expectRevert(UserAlreadyHasAccountException.selector); - // creditManager.transferAccountOwnership(FRIEND, USER); - } - - /// @dev [CM-24]: transferAccountOwnership changes creditAccounts map properly - - function test_CM_24_transferAccountOwnership_changes_creditAccounts_map_properly() public { - (,,, address creditAccount) = _openCreditAccount(); - - creditManager.transferAccountOwnership(creditAccount, FRIEND); - - // assertEq(creditManager.creditAccounts(USER), address(0), "From account wasn't deleted"); - - // assertEq(creditManager.creditAccounts(FRIEND), creditAccount, "To account isn't correct"); - - // vm.expectRevert(CreditAccountNotExistsException.selector); - // creditManager.getCreditAccountOrRevert(USER); - } - - // - // APPROVE CREDIT ACCOUNT - // - - /// @dev [CM-25A]: approveCreditAccount reverts if the token is not added - function test_CM_25A_approveCreditAccount_reverts_if_the_token_is_not_added() public { - (,,, address creditAccount) = _openCreditAccount(); - creditManager.setCreditAccountForExternalCall(creditAccount); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - vm.expectRevert(TokenNotAllowedException.selector); - - vm.prank(ADAPTER); - creditManager.approveCreditAccount(DUMB_ADDRESS, 100); - } - - /// @dev [CM-26]: approveCreditAccount approves with desired allowance - function test_CM_26_approveCreditAccount_approves_with_desired_allowance() public { - (,,, address creditAccount) = _openCreditAccount(); - creditManager.setCreditAccountForExternalCall(creditAccount); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - // Case, when current allowance > Allowance_THRESHOLD - tokenTestSuite.approve(Tokens.DAI, creditAccount, DUMB_ADDRESS, 200); - - address dai = tokenTestSuite.addressOf(Tokens.DAI); - - vm.prank(ADAPTER); - creditManager.approveCreditAccount(dai, DAI_EXCHANGE_AMOUNT); - - expectAllowance(Tokens.DAI, creditAccount, DUMB_ADDRESS, DAI_EXCHANGE_AMOUNT); - } - - /// @dev [CM-27A]: approveCreditAccount works for ERC20 that revert if allowance > 0 before approve - function test_CM_27A_approveCreditAccount_works_for_ERC20_with_approve_restrictions() public { - (,,, address creditAccount) = _openCreditAccount(); - creditManager.setCreditAccountForExternalCall(creditAccount); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - address approveRevertToken = address(new ERC20ApproveRestrictedRevert()); - - vm.prank(CONFIGURATOR); - creditManager.addToken(approveRevertToken); - - vm.prank(ADAPTER); - creditManager.approveCreditAccount(approveRevertToken, DAI_EXCHANGE_AMOUNT); - - vm.prank(ADAPTER); - creditManager.approveCreditAccount(approveRevertToken, 2 * DAI_EXCHANGE_AMOUNT); - - expectAllowance(approveRevertToken, creditAccount, DUMB_ADDRESS, 2 * DAI_EXCHANGE_AMOUNT); - } - - // /// @dev [CM-27B]: approveCreditAccount works for ERC20 that returns false if allowance > 0 before approve - function test_CM_27B_approveCreditAccount_works_for_ERC20_with_approve_restrictions() public { - (,,, address creditAccount) = _openCreditAccount(); - creditManager.setCreditAccountForExternalCall(creditAccount); - - address approveFalseToken = address(new ERC20ApproveRestrictedFalse()); - - vm.prank(CONFIGURATOR); - creditManager.addToken(approveFalseToken); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - vm.prank(ADAPTER); - creditManager.approveCreditAccount(approveFalseToken, DAI_EXCHANGE_AMOUNT); - - vm.prank(ADAPTER); - creditManager.approveCreditAccount(approveFalseToken, 2 * DAI_EXCHANGE_AMOUNT); - - expectAllowance(approveFalseToken, creditAccount, DUMB_ADDRESS, 2 * DAI_EXCHANGE_AMOUNT); - } - - // - // EXECUTE ORDER - // - - /// @dev [CM-29]: executeOrder calls credit account method and emit event - function test_CM_29_executeOrder_calls_credit_account_method_and_emit_event() public { - (,,, address creditAccount) = _openCreditAccount(); - creditManager.setCreditAccountForExternalCall(creditAccount); - - TargetContractMock targetMock = new TargetContractMock(); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, address(targetMock)); - - bytes memory callData = bytes("Hello, world!"); - - // we emit the event we expect to see. - vm.expectEmit(true, false, false, false); - emit ExecuteOrder(address(targetMock)); - - // stack trace check - vm.expectCall(creditAccount, abi.encodeWithSignature("execute(address,bytes)", address(targetMock), callData)); - vm.expectCall(address(targetMock), callData); - - vm.prank(ADAPTER); - creditManager.executeOrder(callData); - - assertEq0(targetMock.callData(), callData, "Incorrect calldata"); - } - - // - // FULL COLLATERAL CHECK - // - - /// @dev [CM-38]: fullCollateralCheck skips tokens is they are not enabled - function test_CM_38_fullCollateralCheck_skips_tokens_is_they_are_not_enabled() public { - address creditAccount = _openAccountAndTransferToCF(); - - tokenTestSuite.mint(Tokens.USDC, creditAccount, USDC_ACCOUNT_AMOUNT); - - vm.expectRevert(NotEnoughCollateralException.selector); - _baseFullCollateralCheck(creditAccount); - - // fullCollateralCheck doesn't revert when token is enabled - uint256 usdcTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)); - uint256 daiTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)); - - creditManager.fullCollateralCheck(creditAccount, usdcTokenMask | daiTokenMask, new uint256[](0), 10000); - } - - /// @dev [CM-39]: fullCollateralCheck diables tokens if they have zero balance - function test_CM_39_fullCollateralCheck_diables_tokens_if_they_have_zero_balance() public { - (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow, address creditAccount) = - _openCreditAccount(); - creditManager.transferAccountOwnership(creditAccount, address(this)); - - (uint256 feeInterest,,,,) = creditManager.fees(); - - /// TODO: CHANGE COMPUTATION - - uint256 borrowAmountWithInterest = borrowedAmount * cumulativeIndexNow / cumulativeIndexLastUpdate; - uint256 interestAccured = borrowAmountWithInterest - borrowedAmount; - - uint256 amountToRepayInLINK = ( - ((borrowAmountWithInterest + interestAccured * feeInterest / PERCENTAGE_FACTOR) * (10 ** 8)) - * PERCENTAGE_FACTOR / tokenTestSuite.prices(Tokens.LINK) - / creditManager.liquidationThresholds(tokenTestSuite.addressOf(Tokens.LINK)) - ) + 1000000000; - - tokenTestSuite.mint(Tokens.LINK, creditAccount, amountToRepayInLINK); - - uint256 wethTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - uint256 daiTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)); - uint256 linkTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - // Enable WETH and LINK token. WETH should be disabled adter fullCollateralCheck - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.LINK)); - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.WETH)); - - creditManager.fullCollateralCheck( - creditAccount, wethTokenMask | linkTokenMask | daiTokenMask, new uint256[](0), 10000 - ); - - expectTokenIsEnabled(creditAccount, Tokens.LINK, true); - expectTokenIsEnabled(creditAccount, Tokens.WETH, false); - } - - /// @dev [CM-40]: fullCollateralCheck breaks loop if total >= borrowAmountPlusInterestRateUSD and pass the check - function test_CM_40_fullCollateralCheck_breaks_loop_if_total_gte_borrowAmountPlusInterestRateUSD_and_pass_the_check( - ) public { - vm.startPrank(CONFIGURATOR); - - CreditManagerV3 cm = new CreditManagerV3(address(poolMock), address(withdrawalManager)); - cms.cr().addCreditManager(address(cm)); - - cm.setCreditFacade(address(this)); - cm.setPriceOracle(address(priceOracle)); - - vm.stopPrank(); - - address creditAccount = cm.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, false); - cm.transferAccountOwnership(creditAccount, address(this)); - - address revertToken = DUMB_ADDRESS; - address linkToken = tokenTestSuite.addressOf(Tokens.LINK); - - // We add "revert" token - DUMB address which would revert if balanceOf method would be called - // If (total >= borrowAmountPlusInterestRateUSD) doesn't break the loop, it would be called - // cause we enable this token using checkAndEnableToken. - // If fullCollateralCheck doesn't revert, it means that the break works - vm.startPrank(CONFIGURATOR); - - cm.addToken(linkToken); - cm.addToken(revertToken); - cm.setCollateralTokenData( - linkToken, creditConfig.lt(Tokens.LINK), creditConfig.lt(Tokens.LINK), type(uint40).max, 0 - ); - - vm.stopPrank(); - - // cm.checkAndEnableToken(revertToken); - // cm.checkAndEnableToken(linkToken); - - // We add WAD for rounding compensation - uint256 amountToRepayInLINK = ((DAI_ACCOUNT_AMOUNT + WAD) * PERCENTAGE_FACTOR * (10 ** 8)) - / creditConfig.lt(Tokens.LINK) / tokenTestSuite.prices(Tokens.LINK); - - tokenTestSuite.mint(Tokens.LINK, creditAccount, amountToRepayInLINK); - - uint256 revertTokenMask = cm.getTokenMaskOrRevert(revertToken); - uint256 linkTokenMask = cm.getTokenMaskOrRevert(linkToken); - - uint256 enabledTokensMap = revertTokenMask | linkTokenMask; - - cm.fullCollateralCheck(creditAccount, enabledTokensMap, new uint256[](0), 10000); - } - - /// @dev [CM-41]: fullCollateralCheck reverts if CA has more than allowed enabled tokens - function test_CM_41_fullCollateralCheck_reverts_if_CA_has_more_than_allowed_enabled_tokens() public { - vm.startPrank(CONFIGURATOR); - - // We use clean CreditManagerV3 to have only one underlying token for testing - creditManager = new CreditManagerV3(address(poolMock), address(withdrawalManager)); - cms.cr().addCreditManager(address(creditManager)); - - creditManager.setCreditFacade(address(this)); - creditManager.setPriceOracle(address(priceOracle)); - - creditManager.setCollateralTokenData(poolMock.underlyingToken(), 9300, 9300, type(uint40).max, 0); - vm.stopPrank(); - - address creditAccount = creditManager.openCreditAccount(DAI_ACCOUNT_AMOUNT, address(this), false); - tokenTestSuite.mint(Tokens.DAI, creditAccount, 2 * DAI_ACCOUNT_AMOUNT); - - enableTokensMoreThanLimit(creditAccount); - vm.expectRevert(TooManyEnabledTokensException.selector); - - creditManager.fullCollateralCheck(creditAccount, 2 ** 13 - 1, new uint256[](0), 10000); - } - - /// @dev [CM-41A]: fullCollateralCheck correctly disables the underlying when needed - function test_CM_41A_fullCollateralCheck_correctly_dfisables_the_underlying_when_needed() public { - (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow, address creditAccount) = - _openCreditAccount(); - - uint256 daiBalance = tokenTestSuite.balanceOf(Tokens.DAI, creditAccount); - - tokenTestSuite.burn(Tokens.DAI, creditAccount, daiBalance); - - _addAndEnableTokens(creditAccount, 200, 0); - - uint256 totalTokens = creditManager.collateralTokensCount(); - - uint256 borrowAmountWithInterest = borrowedAmount * cumulativeIndexNow / cumulativeIndexLastUpdate; - uint256 interestAccured = borrowAmountWithInterest - borrowedAmount; - - (uint256 feeInterest,,,,) = creditManager.fees(); - - uint256 amountToRepayInLINK = ( - ((borrowAmountWithInterest + interestAccured * feeInterest / PERCENTAGE_FACTOR) * (10 ** 8)) - * PERCENTAGE_FACTOR / tokenTestSuite.prices(Tokens.DAI) - / creditManager.liquidationThresholds(tokenTestSuite.addressOf(Tokens.DAI)) - ) + WAD; - - tokenTestSuite.mint(Tokens.DAI, creditAccount, amountToRepayInLINK); - - uint256[] memory hints = new uint256[](totalTokens); - unchecked { - for (uint256 i; i < totalTokens; ++i) { - hints[i] = 2 ** (totalTokens - i - 1); - } - } - - creditManager.fullCollateralCheck(creditAccount, 2 ** (totalTokens) - 1, hints, 10000); - - assertEq( - creditManager.enabledTokensMaskOf(creditAccount).calcEnabledTokens(), - 1, - "Incorrect number of tokens enabled" - ); - } - - /// @dev [CM-42]: fullCollateralCheck fuzzing test - function test_CM_42_fullCollateralCheck_fuzzing_test( - uint128 borrowedAmount, - uint128 daiBalance, - uint128 usdcBalance, - uint128 linkBalance, - uint128 wethBalance, - bool enableUSDC, - bool enableLINK, - bool enableWETH, - uint16 minHealthFactor - ) public { - // vm.assume(borrowedAmount > WAD); - - // vm.assume(minHealthFactor > 10_000 && minHealthFactor < 50_000); - - // tokenTestSuite.mint(Tokens.DAI, address(poolMock), borrowedAmount); - - // (,,, address creditAccount) = cms.openCreditAccount(borrowedAmount); - // creditManager.transferAccountOwnership(creditAccount, address(this)); - - // if (daiBalance > borrowedAmount) { - // tokenTestSuite.mint(Tokens.DAI, creditAccount, daiBalance - borrowedAmount); - // } else { - // tokenTestSuite.burn(Tokens.DAI, creditAccount, borrowedAmount - daiBalance); - // } - - // expectBalance(Tokens.DAI, creditAccount, daiBalance); - - // mintBalance(creditAccount, Tokens.USDC, usdcBalance, enableUSDC); - // mintBalance(creditAccount, Tokens.LINK, linkBalance, enableLINK); - // mintBalance(creditAccount, Tokens.WETH, wethBalance, enableWETH); - - // uint256 twvUSD = ( - // tokenTestSuite.balanceOf(Tokens.DAI, creditAccount) * tokenTestSuite.prices(Tokens.DAI) - // * creditConfig.lt(Tokens.DAI) - // ) / WAD; - - // twvUSD += !enableUSDC - // ? 0 - // : ( - // tokenTestSuite.balanceOf(Tokens.USDC, creditAccount) * tokenTestSuite.prices(Tokens.USDC) - // * creditConfig.lt(Tokens.USDC) - // ) / (10 ** 6); - - // twvUSD += !enableLINK - // ? 0 - // : ( - // tokenTestSuite.balanceOf(Tokens.LINK, creditAccount) * tokenTestSuite.prices(Tokens.LINK) - // * creditConfig.lt(Tokens.LINK) - // ) / WAD; - - // twvUSD += !enableWETH - // ? 0 - // : ( - // tokenTestSuite.balanceOf(Tokens.WETH, creditAccount) * tokenTestSuite.prices(Tokens.WETH) - // * creditConfig.lt(Tokens.WETH) - // ) / WAD; - - // (,, uint256 borrowedAmountWithInterestAndFees) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // uint256 debtUSD = borrowedAmountWithInterestAndFees * tokenTestSuite.prices(Tokens.DAI) * minHealthFactor / WAD; - - // bool shouldRevert = twvUSD < debtUSD; - - // uint256 enabledTokensMap = 1; - - // if (enableUSDC) { - // enabledTokensMap |= creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)); - // } - - // if (enableLINK) { - // enabledTokensMap |= creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - // } - - // if (enableWETH) { - // enabledTokensMap |= creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - // } - - // if (shouldRevert) { - // vm.expectRevert(NotEnoughCollateralException.selector); - // } - - // creditManager.fullCollateralCheck(creditAccount, enabledTokensMap, new uint256[](0), minHealthFactor); - } - - // - // CALC CLOSE PAYMENT PURE - // - struct CalcClosePaymentsPureTestCase { - string name; - uint256 totalValue; - ClosureAction closureActionType; - uint256 borrowedAmount; - uint256 borrowedAmountWithInterest; - uint256 amountToPool; - uint256 remainingFunds; - uint256 profit; - uint256 loss; - } - - /// @dev [CM-43]: calcClosePayments computes - function test_CM_43_calcClosePayments_test() public { - // vm.prank(CONFIGURATOR); - - // creditManager.setParams( - // 1000, // feeInterest: 10% , it doesn't matter this test - // 200, // feeLiquidation: 2%, it doesn't matter this test - // 9500, // liquidationPremium: 5%, it doesn't matter this test - // 100, // feeLiquidationExpired: 1% - // 9800 // liquidationPremiumExpired: 2% - // ); - - // CalcClosePaymentsPureTestCase[7] memory cases = [ - // CalcClosePaymentsPureTestCase({ - // name: "CLOSURE", - // totalValue: 0, - // closureActionType: ClosureAction.CLOSE_ACCOUNT, - // borrowedAmount: 1000, - // borrowedAmountWithInterest: 1100, - // amountToPool: 1110, // amountToPool = 1100 + 100 * 10% = 1110 - // remainingFunds: 0, - // profit: 10, // profit: 100 (interest) * 10% = 10 - // loss: 0 - // }), - // CalcClosePaymentsPureTestCase({ - // name: "LIQUIDATION WITH PROFIT & REMAINING FUNDS", - // totalValue: 2000, - // closureActionType: ClosureAction.LIQUIDATE_ACCOUNT, - // borrowedAmount: 1000, - // borrowedAmountWithInterest: 1100, - // amountToPool: 1150, // amountToPool = 1100 + 100 * 10% + 2000 * 2% = 1150 - // remainingFunds: 749, //remainingFunds: 2000 * (100% - 5%) - 1150 - 1 = 749 - // profit: 50, - // loss: 0 - // }), - // CalcClosePaymentsPureTestCase({ - // name: "LIQUIDATION WITH PROFIT & ZERO REMAINING FUNDS", - // totalValue: 2100, - // closureActionType: ClosureAction.LIQUIDATE_ACCOUNT, - // borrowedAmount: 900, - // borrowedAmountWithInterest: 1900, - // amountToPool: 1995, // amountToPool = 1900 + 1000 * 10% + 2100 * 2% = 2042, totalFunds = 2100 * 95% = 1995, so, amount to pool would be 1995 - // remainingFunds: 0, // remainingFunds: 2000 * (100% - 5%) - 1150 - 1 = 749 - // profit: 95, - // loss: 0 - // }), - // CalcClosePaymentsPureTestCase({ - // name: "LIQUIDATION WITH LOSS", - // totalValue: 1000, - // closureActionType: ClosureAction.LIQUIDATE_ACCOUNT, - // borrowedAmount: 900, - // borrowedAmountWithInterest: 1900, - // amountToPool: 950, // amountToPool = 1900 + 1000 * 10% + 1000 * 2% = 2020, totalFunds = 1000 * 95% = 950, So, amount to pool would be 950 - // remainingFunds: 0, // 0, cause it's loss - // profit: 0, - // loss: 950 - // }), - // CalcClosePaymentsPureTestCase({ - // name: "LIQUIDATION OF EXPIRED WITH PROFIT & REMAINING FUNDS", - // totalValue: 2000, - // closureActionType: ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT, - // borrowedAmount: 1000, - // borrowedAmountWithInterest: 1100, - // amountToPool: 1130, // amountToPool = 1100 + 100 * 10% + 2000 * 1% = 1130 - // remainingFunds: 829, //remainingFunds: 2000 * (100% - 2%) - 1130 - 1 = 829 - // profit: 30, - // loss: 0 - // }), - // CalcClosePaymentsPureTestCase({ - // name: "LIQUIDATION OF EXPIRED WITH PROFIT & ZERO REMAINING FUNDS", - // totalValue: 2100, - // closureActionType: ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT, - // borrowedAmount: 900, - // borrowedAmountWithInterest: 2000, - // amountToPool: 2058, // amountToPool = 2000 + 1100 * 10% + 2100 * 1% = 2131, totalFunds = 2100 * 98% = 2058, so, amount to pool would be 2058 - // remainingFunds: 0, - // profit: 58, - // loss: 0 - // }), - // CalcClosePaymentsPureTestCase({ - // name: "LIQUIDATION OF EXPIRED WITH LOSS", - // totalValue: 1000, - // closureActionType: ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT, - // borrowedAmount: 900, - // borrowedAmountWithInterest: 1900, - // amountToPool: 980, // amountToPool = 1900 + 1000 * 10% + 1000 * 2% = 2020, totalFunds = 1000 * 98% = 980, So, amount to pool would be 980 - // remainingFunds: 0, // 0, cause it's loss - // profit: 0, - // loss: 920 - // }) - // // CalcClosePaymentsPureTestCase({ - // // name: "LIQUIDATION WHILE PAUSED WITH REMAINING FUNDS", - // // totalValue: 2000, - // // closureActionType: ClosureAction.LIQUIDATE_PAUSED, - // // borrowedAmount: 1000, - // // borrowedAmountWithInterest: 1100, - // // amountToPool: 1150, // amountToPool = 1100 + 100 * 10% + 2000 * 2% = 1150 - // // remainingFunds: 849, //remainingFunds: 2000 - 1150 - 1 = 869 - // // profit: 50, - // // loss: 0 - // // }), - // // CalcClosePaymentsPureTestCase({ - // // name: "LIQUIDATION OF EXPIRED WITH LOSS", - // // totalValue: 1000, - // // closureActionType: ClosureAction.LIQUIDATE_PAUSED, - // // borrowedAmount: 900, - // // borrowedAmountWithInterest: 1900, - // // amountToPool: 1000, // amountToPool = 1900 + 1000 * 10% + 1000 * 2% = 2020, totalFunds = 1000 * 98% = 980, So, amount to pool would be 980 - // // remainingFunds: 0, // 0, cause it's loss - // // profit: 0, - // // loss: 900 - // // }) - // ]; - - // for (uint256 i = 0; i < cases.length; i++) { - // (uint256 amountToPool, uint256 remainingFunds, uint256 profit, uint256 loss) = creditManager - // .calcClosePayments( - // cases[i].totalValue, - // cases[i].closureActionType, - // cases[i].borrowedAmount, - // cases[i].borrowedAmountWithInterest - // ); - - // assertEq(amountToPool, cases[i].amountToPool, string(abi.encodePacked(cases[i].name, ": amountToPool"))); - // assertEq( - // remainingFunds, cases[i].remainingFunds, string(abi.encodePacked(cases[i].name, ": remainingFunds")) - // ); - // assertEq(profit, cases[i].profit, string(abi.encodePacked(cases[i].name, ": profit"))); - // assertEq(loss, cases[i].loss, string(abi.encodePacked(cases[i].name, ": loss"))); - // } - } - - // - // TRASNFER ASSETS TO - // - - /// @dev [CM-44]: _transferAssetsTo sends all tokens except underlying one and not-enabled to provided address - function test_CM_44_transferAssetsTo_sends_all_tokens_except_underlying_one_to_provided_address() public { - // It enables CreditManagerTestInternal for some test cases - _connectCreditManagerSuite(Tokens.DAI, true); - - address[2] memory friends = [FRIEND, FRIEND2]; - - // CASE 0: convertToETH = false - // CASE 1: convertToETH = true - for (uint256 i = 0; i < 2; i++) { - bool convertToETH = i > 0; - - address friend = friends[i]; - (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); - creditManager.transferAccountOwnership(creditAccount, address(this)); - - CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); - - tokenTestSuite.mint(Tokens.USDC, creditAccount, USDC_EXCHANGE_AMOUNT); - tokenTestSuite.mint(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); - tokenTestSuite.mint(Tokens.LINK, creditAccount, LINK_EXCHANGE_AMOUNT); - - address wethTokenAddr = tokenTestSuite.addressOf(Tokens.WETH); - // creditManager.checkAndEnableToken(wethTokenAddr); - - uint256 enabledTokensMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)) - | creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - - cmi.transferAssetsTo(creditAccount, friend, convertToETH, enabledTokensMask); - - expectBalance(Tokens.DAI, creditAccount, borrowedAmount, "Underlying assets were transffered!"); - - expectBalance(Tokens.DAI, friend, 0); - - expectBalance(Tokens.USDC, creditAccount, USDC_EXCHANGE_AMOUNT); - - expectBalance(Tokens.USDC, friend, 0); - - expectBalance(Tokens.WETH, creditAccount, 1); - - if (convertToETH) { - assertEq( - wethGateway.balanceOf(friend), - WETH_EXCHANGE_AMOUNT - 1, - "Incorrect amount were sent to friend address" - ); - } else { - expectBalance(Tokens.WETH, friend, WETH_EXCHANGE_AMOUNT - 1); - } - - expectBalance(Tokens.LINK, creditAccount, LINK_EXCHANGE_AMOUNT); - - expectBalance(Tokens.LINK, friend, 0); - - creditManager.transferAccountOwnership(creditAccount, USER); - // creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.LIQUIDATE_ACCOUNT, - // 0, - // LIQUIDATOR, - // friend, - // enabledTokensMask, - // 0, - // DAI_ACCOUNT_AMOUNT, - // false - // ); - } - } - - // - // SAFE TOKEN TRANSFER - // - - /// @dev [CM-45]: _safeTokenTransfer transfers tokens - function test_CM_45_safeTokenTransfer_transfers_tokens() public { - // It enables CreditManagerTestInternal for some test cases - _connectCreditManagerSuite(Tokens.DAI, true); - - uint256 WETH_TRANSFER = WETH_EXCHANGE_AMOUNT / 4; - - address[2] memory friends = [FRIEND, FRIEND2]; - - // CASE 0: convertToETH = false - // CASE 1: convertToETH = true - for (uint256 i = 0; i < 2; i++) { - bool convertToETH = i > 0; - - address friend = friends[i]; - (,,, address creditAccount) = _openCreditAccount(); - - CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); - - tokenTestSuite.mint(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); - - cmi.safeTokenTransfer( - creditAccount, tokenTestSuite.addressOf(Tokens.WETH), friend, WETH_TRANSFER, convertToETH - ); - - expectBalance(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT - WETH_TRANSFER); - - if (convertToETH) { - assertEq(wethGateway.balanceOf(friend), WETH_TRANSFER, "Incorrect amount were sent to friend address"); - } else { - expectBalance(Tokens.WETH, friend, WETH_TRANSFER); - } - - // creditManager.closeCreditAccount( - // creditAccount, ClosureAction.LIQUIDATE_ACCOUNT, 0, LIQUIDATOR, friend, 1, 0, DAI_ACCOUNT_AMOUNT, false - // ); - } - } - - // - // DISABLE TOKEN - // - - // /// @dev [CM-46]: _disableToken disabale tokens and do not enable it if called twice - // function test_CM_46__disableToken_disabale_tokens_and_do_not_enable_it_if_called_twice() public { - // // It enables CreditManagerTestInternal for some test cases - // _connectCreditManagerSuite(Tokens.DAI, true); - - // address creditAccount = _openAccountAndTransferToCF(); - - // address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - // // creditManager.checkAndEnableToken(usdcToken); - - // expectTokenIsEnabled(creditAccount, Tokens.USDC, true); - - // CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); - - // cmi.disableToken(usdcToken); - // expectTokenIsEnabled(creditAccount, Tokens.USDC, false); - - // cmi.disableToken(usdcToken); - // expectTokenIsEnabled(creditAccount, Tokens.USDC, false); - // } - - /// @dev [CM-47]: collateralTokens works as expected - function test_CM_47_collateralTokens_works_as_expected(address newToken, uint16 newLT) public { - // vm.assume(newToken != underlying && newToken != address(0)); - - // vm.startPrank(CONFIGURATOR); - - // // reset connected tokens - // CreditManagerV3 cm = new CreditManagerV3(address(poolMock), address(0)); - - // cm.setLiquidationThreshold(underlying, 9200); - - // (address token, uint16 lt) = cm.collateralTokens(0); - // assertEq(token, underlying, "incorrect underlying token"); - // assertEq(lt, 9200, "incorrect lt for underlying token"); - - // uint16 ltAlt = cm.liquidationThresholds(underlying); - // assertEq(ltAlt, 9200, "incorrect lt for underlying token"); - - // assertEq(cm.collateralTokensCount(), 1, "Incorrect length"); - - // cm.addToken(newToken); - // assertEq(cm.collateralTokensCount(), 2, "Incorrect length"); - // (token, lt) = cm.collateralTokens(1); - - // assertEq(token, newToken, "incorrect newToken token"); - // assertEq(lt, 0, "incorrect lt for newToken token"); - - // cm.setLiquidationThreshold(newToken, newLT); - // (token, lt) = cm.collateralTokens(1); - - // assertEq(token, newToken, "incorrect newToken token"); - // assertEq(lt, newLT, "incorrect lt for newToken token"); - - // ltAlt = cm.liquidationThresholds(newToken); - - // assertEq(ltAlt, newLT, "incorrect lt for newToken token"); - - // vm.stopPrank(); - } - - // - // GET CREDIT ACCOUNT OR REVERT - // - - /// @dev [CM-48]: getCreditAccountOrRevert reverts if borrower has no account - // function test_CM_48_getCreditAccountOrRevert_reverts_if_borrower_has_no_account() public { - // (,,, address creditAccount) = _openCreditAccount(); - - // assertEq(creditManager.getCreditAccountOrRevert(USER), creditAccount, "Incorrect credit account"); - - // vm.expectRevert(CreditAccountNotExistsException.selector); - // creditManager.getCreditAccountOrRevert(DUMB_ADDRESS); - // } - - // - // CALC CREDIT ACCOUNT ACCRUED INTEREST - // - - /// @dev [CM-49]: calcCreditAccountAccruedInterest computes correctly - function test_CM_49_calcCreditAccountAccruedInterest_computes_correctly(uint128 amount) public { - // tokenTestSuite.mint(Tokens.DAI, address(poolMock), amount); - // (,,, address creditAccount) = cms.openCreditAccount(amount); - - // uint256 expectedBorrowedAmount = amount; - - // (, uint256 cumulativeIndexLastUpdate,,,,) = creditManager.creditAccountInfo(creditAccount); - - // uint256 cumulativeIndexNow = poolMock._cumulativeIndex_RAY(); - // uint256 expectedBorrowedAmountWithInterest = - // (expectedBorrowedAmount * cumulativeIndexNow) / cumulativeIndexLastUpdate; - - // (uint256 feeInterest,,,,) = creditManager.fees(); - - // uint256 expectedFee = - // ((expectedBorrowedAmountWithInterest - expectedBorrowedAmount) * feeInterest) / PERCENTAGE_FACTOR; - - // (uint256 borrowedAmount, uint256 borrowedAmountWithInterest, uint256 borrowedAmountWithInterestAndFees) = - // creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // assertEq(borrowedAmount, expectedBorrowedAmount, "Incorrect borrowed amount"); - // assertEq( - // borrowedAmountWithInterest, expectedBorrowedAmountWithInterest, "Incorrect borrowed amount with interest" - // ); - // assertEq( - // borrowedAmountWithInterestAndFees, - // expectedBorrowedAmountWithInterest + expectedFee, - // "Incorrect borrowed amount with interest and fees" - // ); - } - - // - // GET CREDIT ACCOUNT PARAMETERS - // - - /// @dev [CM-50]: getCreditAccountParameters return correct values - function test_CM_50_getCreditAccountParameters_return_correct_values() public { - // It enables CreditManagerTestInternal for some test cases - _connectCreditManagerSuite(Tokens.DAI, true); - - (,,, address creditAccount) = _openCreditAccount(); - - (uint256 expectedDebt, uint256 expectedcumulativeIndexLastUpdate,,,,) = - creditManager.creditAccountInfo(creditAccount); - - CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); - - (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate,) = cmi.getCreditAccountParameters(creditAccount); - - assertEq(borrowedAmount, expectedDebt, "Incorrect borrowed amount"); - assertEq(cumulativeIndexLastUpdate, expectedcumulativeIndexLastUpdate, "Incorrect cumulativeIndexLastUpdate"); - - assertEq(cumulativeIndexLastUpdate, expectedcumulativeIndexLastUpdate, "cumulativeIndexLastUpdate"); - } - - // - // SET PARAMS - // - - /// @dev [CM-51]: setParams sets configuration properly - function test_CM_51_setParams_sets_configuration_properly() public { - uint16 s_feeInterest = 8733; - uint16 s_feeLiquidation = 1233; - uint16 s_liquidationPremium = 1220; - uint16 s_feeLiquidationExpired = 1221; - uint16 s_liquidationPremiumExpired = 7777; - - vm.prank(CONFIGURATOR); - creditManager.setParams( - s_feeInterest, s_feeLiquidation, s_liquidationPremium, s_feeLiquidationExpired, s_liquidationPremiumExpired - ); - ( - uint16 feeInterest, - uint16 feeLiquidation, - uint16 liquidationDiscount, - uint16 feeLiquidationExpired, - uint16 liquidationPremiumExpired - ) = creditManager.fees(); - - assertEq(feeInterest, s_feeInterest, "Incorrect feeInterest"); - assertEq(feeLiquidation, s_feeLiquidation, "Incorrect feeLiquidation"); - assertEq(liquidationDiscount, s_liquidationPremium, "Incorrect liquidationDiscount"); - assertEq(feeLiquidationExpired, s_feeLiquidationExpired, "Incorrect feeLiquidationExpired"); - assertEq(liquidationPremiumExpired, s_liquidationPremiumExpired, "Incorrect liquidationPremiumExpired"); - } - - // - // ADD TOKEN - // - - /// @dev [CM-52]: addToken reverts if token exists and if collateralTokens > 256 - function test_CM_52_addToken_reverts_if_token_exists_and_if_collateralTokens_more_256() public { - vm.startPrank(CONFIGURATOR); - - vm.expectRevert(TokenAlreadyAddedException.selector); - creditManager.addToken(underlying); - - for (uint256 i = creditManager.collateralTokensCount(); i < 248; i++) { - creditManager.addToken(address(uint160(uint256(keccak256(abi.encodePacked(i)))))); - } - - vm.expectRevert(TooManyTokensException.selector); - creditManager.addToken(DUMB_ADDRESS); - - vm.stopPrank(); - } - - /// @dev [CM-53]: addToken adds token and set tokenMaskMap correctly - function test_CM_53_addToken_adds_token_and_set_tokenMaskMap_correctly() public { - uint256 count = creditManager.collateralTokensCount(); - - vm.prank(CONFIGURATOR); - creditManager.addToken(DUMB_ADDRESS); - - assertEq(creditManager.collateralTokensCount(), count + 1, "collateralTokensCount want incremented"); - - assertEq(creditManager.getTokenMaskOrRevert(DUMB_ADDRESS), 1 << count, "tokenMaskMap was set incorrectly"); - } - - // - // SET LIQUIDATION THRESHOLD - // - - /// @dev [CM-54]: setLiquidationThreshold reverts for unknown token - function test_CM_54_setLiquidationThreshold_reverts_for_unknown_token() public { - vm.prank(CONFIGURATOR); - vm.expectRevert(TokenNotAllowedException.selector); - creditManager.setCollateralTokenData(DUMB_ADDRESS, 8000, 8000, type(uint40).max, 0); - } - - // // - // // SET FORBID MASK - // // - // /// @dev [CM-55]: setForbidMask sets forbidMask correctly - // function test_CM_55_setForbidMask_sets_forbidMask_correctly() public { - // uint256 expectedForbidMask = 244; - - // assertTrue(creditManager.forbiddenTokenMask() != expectedForbidMask, "expectedForbidMask is already the same"); - - // vm.prank(CONFIGURATOR); - // creditManager.setForbidMask(expectedForbidMask); - - // assertEq(creditManager.forbiddenTokenMask(), expectedForbidMask, "ForbidMask is not set correctly"); - // } - - // - // CHANGE CONTRACT AllowanceAction - // - - /// @dev [CM-56]: setContractAllowance updates adapterToContract - function test_CM_56_setContractAllowance_updates_adapterToContract() public { - assertTrue( - creditManager.adapterToContract(ADAPTER) != DUMB_ADDRESS, "adapterToContract(ADAPTER) is already the same" - ); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - assertEq(creditManager.adapterToContract(ADAPTER), DUMB_ADDRESS, "adapterToContract is not set correctly"); - - assertEq(creditManager.contractToAdapter(DUMB_ADDRESS), ADAPTER, "adapterToContract is not set correctly"); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, address(0)); - - assertEq(creditManager.adapterToContract(ADAPTER), address(0), "adapterToContract is not set correctly"); - - assertEq(creditManager.contractToAdapter(address(0)), address(0), "adapterToContract is not set correctly"); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(address(0), DUMB_ADDRESS); - - assertEq(creditManager.adapterToContract(address(0)), address(0), "adapterToContract is not set correctly"); - - assertEq(creditManager.contractToAdapter(DUMB_ADDRESS), address(0), "adapterToContract is not set correctly"); - - // vm.prank(CONFIGURATOR); - // creditManager.setContractAllowance(ADAPTER, UNIVERSAL_CONTRACT); - - // assertEq(creditManager.universalAdapter(), ADAPTER, "Universal adapter is not correctly set"); - - // vm.prank(CONFIGURATOR); - // creditManager.setContractAllowance(address(0), UNIVERSAL_CONTRACT); - - // assertEq(creditManager.universalAdapter(), address(0), "Universal adapter is not correctly set"); - } - - // - // UPGRADE CONTRACTS - // - - /// @dev [CM-57A]: setCreditFacade updates Credit Facade correctly - function test_CM_57A_setCreditFacade_updates_contract_correctly() public { - assertTrue(creditManager.creditFacade() != DUMB_ADDRESS, "creditFacade( is already the same"); - - vm.prank(CONFIGURATOR); - creditManager.setCreditFacade(DUMB_ADDRESS); - - assertEq(creditManager.creditFacade(), DUMB_ADDRESS, "creditFacade is not set correctly"); - } - - /// @dev [CM-57B]: setPriceOracle updates contract correctly - function test_CM_57_setPriceOracle_updates_contract_correctly() public { - assertTrue(address(creditManager.priceOracle()) != DUMB_ADDRESS2, "priceOracle is already the same"); - - vm.prank(CONFIGURATOR); - creditManager.setPriceOracle(DUMB_ADDRESS2); - - assertEq(address(creditManager.priceOracle()), DUMB_ADDRESS2, "priceOracle is not set correctly"); - } - - // - // SET CONFIGURATOR - // - - /// @dev [CM-58]: setCreditConfigurator sets creditConfigurator correctly and emits event - function test_CM_58_setCreditConfigurator_sets_creditConfigurator_correctly_and_emits_event() public { - assertTrue(creditManager.creditConfigurator() != DUMB_ADDRESS, "creditConfigurator is already the same"); - - vm.prank(CONFIGURATOR); - - vm.expectEmit(true, false, false, false); - emit SetCreditConfigurator(DUMB_ADDRESS); - - creditManager.setCreditConfigurator(DUMB_ADDRESS); - - assertEq(creditManager.creditConfigurator(), DUMB_ADDRESS, "creditConfigurator is not set correctly"); - } - - // /// @dev [CM-59]: _getTokenIndexByAddress works properly - // function test_CM_59_getMaxIndex_works_properly(uint256 noise) public { - // CreditManagerTestInternal cm = new CreditManagerTestInternal( - // address(poolMock) - // ); - - // for (uint256 i = 0; i < 256; i++) { - // uint256 mask = 1 << i; - // if (mask > noise) mask |= noise; - // uint256 value = cm.getMaxIndex(mask); - // assertEq(i, value, "Incorrect result"); - // } - // } - - // /// @dev [CM-60]: CreditManagerV3 allows approveCreditAccount and executeOrder for universal adapter - // function test_CM_60_universal_adapter_can_call_adapter_restricted_functions() public { - // TargetContractMock targetMock = new TargetContractMock(); - - // vm.prank(CONFIGURATOR); - // creditManager.setContractAllowance(ADAPTER, UNIVERSAL_CONTRACT_ADDRESS); - - // _openAccountAndTransferToCF(); - - // vm.prank(ADAPTER); - // creditManager.approveCreditAccount(DUMB_ADDRESS, underlying, type(uint256).max); - - // bytes memory callData = bytes("Hello"); - - // vm.prank(ADAPTER); - // creditManager.executeOrder(address(targetMock), callData); - // } - - /// @dev [CM-61]: setMaxEnabledToken correctly sets value - function test_CM_61_setMaxEnabledTokens_works_correctly() public { - vm.prank(CONFIGURATOR); - creditManager.setMaxEnabledTokens(255); - - assertEq(creditManager.maxAllowedEnabledTokenLength(), 255, "Incorrect max enabled tokens"); - } - - // /// @dev [CM-64]: closeCreditAccount reverts when attempting to liquidate while paused, - // /// and the payer is not set as emergency liquidator - - // function test_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 [CM-65]: Emergency liquidator can't close an account instead of liquidating - - // function test_CM_65_closeCreditAccount_reverts_when_paused_and_liquidator_tries_to_close() public { - // 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); - // } - - /// @dev [CM-66]: calcNewCumulativeIndex works correctly for various values - function test_CM_66_calcNewCumulativeIndex_is_correct( - uint128 borrowedAmount, - uint256 indexAtOpen, - uint256 indexNow, - uint128 delta, - bool isIncrease - ) public { - // vm.assume(borrowedAmount > 100); - // vm.assume(uint256(borrowedAmount) + uint256(delta) <= 2 ** 128 - 1); - - // indexNow = indexNow < RAY ? indexNow + RAY : indexNow; - // indexAtOpen = indexAtOpen < RAY ? indexAtOpen + RAY : indexNow; - - // vm.assume(indexNow <= 100 * RAY); - // vm.assume(indexNow >= indexAtOpen); - // vm.assume(indexNow - indexAtOpen < 10 * RAY); - - // uint256 interest = uint256((borrowedAmount * indexNow) / indexAtOpen - borrowedAmount); - - // vm.assume(interest > 1); - - // if (!isIncrease && (delta > interest)) delta %= uint128(interest); - - // CreditManagerTestInternal cmi = new CreditManagerTestInternal( - // creditManager.poolService(), address(withdrawalManager) - // ); - - // if (isIncrease) { - // uint256 newIndex = CreditLogic.calcNewCumulativeIndex(borrowedAmount, delta, indexNow, indexAtOpen, true); - - // uint256 newInterestError = ((borrowedAmount + delta) * indexNow) / newIndex - (borrowedAmount + delta) - // - ((borrowedAmount * indexNow) / indexAtOpen - borrowedAmount); - - // uint256 newTotalDebt = ((borrowedAmount + delta) * indexNow) / newIndex; - - // assertLe((RAY * newInterestError) / newTotalDebt, 10000, "Interest error is larger than 10 ** -23"); - // } else { - // uint256 newIndex = cmi.calcNewCumulativeIndex(borrowedAmount, delta, indexNow, indexAtOpen, false); - - // uint256 newTotalDebt = ((borrowedAmount * indexNow) / newIndex); - // uint256 newInterestError = newTotalDebt - borrowedAmount - (interest - delta); - - // emit log_uint(indexNow); - // emit log_uint(indexAtOpen); - // emit log_uint(interest); - // emit log_uint(delta); - // emit log_uint(interest - delta); - // emit log_uint(newTotalDebt); - // emit log_uint(borrowedAmount); - // emit log_uint(newInterestError); - - // assertLe((RAY * newInterestError) / newTotalDebt, 10000, "Interest error is larger than 10 ** -23"); - // } - } - - // /// @dev [CM-67]: checkEmergencyPausable returns pause state and enable emergencyLiquidation if needed - // function test_CM_67_checkEmergencyPausable_returns_pause_state_and_enable_emergencyLiquidation_if_needed() public { - // bool p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, true); - // assertTrue(!p, "Incorrect paused() value for non-paused state"); - // assertTrue(!creditManager.emergencyLiquidation(), "Emergency liquidation true when expected false"); - - // vm.prank(CONFIGURATOR); - // creditManager.pause(); - - // p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, true); - // assertTrue(p, "Incorrect paused() value for paused state"); - // assertTrue(!creditManager.emergencyLiquidation(), "Emergency liquidation true when expected false"); - - // vm.prank(CONFIGURATOR); - // creditManager.unpause(); - - // vm.prank(CONFIGURATOR); - // creditManager.addEmergencyLiquidator(DUMB_ADDRESS); - // p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, true); - // assertTrue(!p, "Incorrect paused() value for non-paused state"); - // assertTrue(!creditManager.emergencyLiquidation(), "Emergency liquidation true when expected false"); - - // vm.prank(CONFIGURATOR); - // creditManager.pause(); - - // p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, true); - // assertTrue(p, "Incorrect paused() value for paused state"); - // assertTrue(creditManager.emergencyLiquidation(), "Emergency liquidation flase when expected true"); - - // p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, false); - // assertTrue(p, "Incorrect paused() value for paused state"); - // assertTrue(!creditManager.emergencyLiquidation(), "Emergency liquidation true when expected false"); - // } - - /// @dev [CM-68]: fullCollateralCheck checks tokens in correct order - function test_CM_68_fullCollateralCheck_is_evaluated_in_order_of_hints() public { - (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow, address creditAccount) = - _openCreditAccount(); - - uint256 daiBalance = tokenTestSuite.balanceOf(Tokens.DAI, creditAccount); - - tokenTestSuite.burn(Tokens.DAI, creditAccount, daiBalance); - - uint256 borrowAmountWithInterest = borrowedAmount * cumulativeIndexNow / cumulativeIndexLastUpdate; - uint256 interestAccured = borrowAmountWithInterest - borrowedAmount; - - (uint256 feeInterest,,,,) = creditManager.fees(); - - uint256 amountToRepay = ( - ((borrowAmountWithInterest + interestAccured * feeInterest / PERCENTAGE_FACTOR) * (10 ** 8)) - * PERCENTAGE_FACTOR / tokenTestSuite.prices(Tokens.DAI) - / creditManager.liquidationThresholds(tokenTestSuite.addressOf(Tokens.DAI)) - ) + WAD; - - tokenTestSuite.mint(Tokens.DAI, creditAccount, amountToRepay); - - tokenTestSuite.mint(Tokens.USDC, creditAccount, USDC_ACCOUNT_AMOUNT); - tokenTestSuite.mint(Tokens.USDT, creditAccount, 10); - tokenTestSuite.mint(Tokens.LINK, creditAccount, 10); - - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.USDC)); - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.USDT)); - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.LINK)); - - uint256[] memory collateralHints = new uint256[](2); - collateralHints[0] = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDT)); - collateralHints[1] = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - - vm.expectCall(tokenTestSuite.addressOf(Tokens.USDT), abi.encodeCall(IERC20.balanceOf, (creditAccount))); - vm.expectCall(tokenTestSuite.addressOf(Tokens.LINK), abi.encodeCall(IERC20.balanceOf, (creditAccount))); - - uint256 enabledTokensMap = 1 | creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)) - | creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDT)) - | creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - - creditManager.fullCollateralCheck(creditAccount, enabledTokensMap, collateralHints, PERCENTAGE_FACTOR); - - // assertEq(cmi.fullCheckOrder(0), tokenTestSuite.addressOf(Tokens.USDT), "Token order incorrect"); - - // assertEq(cmi.fullCheckOrder(1), tokenTestSuite.addressOf(Tokens.LINK), "Token order incorrect"); - - // assertEq(cmi.fullCheckOrder(2), tokenTestSuite.addressOf(Tokens.DAI), "Token order incorrect"); - - // assertEq(cmi.fullCheckOrder(3), tokenTestSuite.addressOf(Tokens.USDC), "Token order incorrect"); - } - - /// @dev [CM-70]: fullCollateralCheck reverts when an illegal mask is passed in collateralHints - function test_CM_70_fullCollateralCheck_reverts_for_illegal_mask_in_hints() public { - (,,, address creditAccount) = _openCreditAccount(); - - vm.expectRevert(TokenNotAllowedException.selector); - - uint256[] memory ch = new uint256[](1); - ch[0] = 3; - - uint256 enabledTokensMap = 1; - - creditManager.fullCollateralCheck(creditAccount, enabledTokensMap, ch, PERCENTAGE_FACTOR); - } - - /// @dev [CM-71]: rampLiquidationThreshold correctly updates the internal struct - function test_CM_71_rampLiquidationThreshold_correctly_updates_parameters() public { - _connectCreditManagerSuite(Tokens.DAI, true); - - address usdc = tokenTestSuite.addressOf(Tokens.USDC); - - CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); - - vm.prank(CONFIGURATOR); - cmi.setCollateralTokenData(usdc, 8500, 9000, uint40(block.timestamp), 3600 * 24 * 7); - - CollateralTokenData memory cd = cmi.collateralTokensDataExt(cmi.getTokenMaskOrRevert(usdc)); - - assertEq(uint256(cd.ltInitial), creditConfig.lt(Tokens.USDC), "Incorrect initial LT"); - - assertEq(uint256(cd.ltFinal), 8500, "Incorrect final LT"); - - assertEq(uint256(cd.timestampRampStart), block.timestamp, "Incorrect timestamp start"); - - assertEq(uint256(cd.rampDuration), 3600 * 24 * 7, "Incorrect ramp duration"); - } - - /// @dev [CM-72]: Ramping liquidation threshold fuzzing - function test_CM_72_liquidation_ramping_fuzzing( - uint16 initialLT, - uint16 newLT, - uint24 duration, - uint256 timestampCheck - ) public { - // initialLT = 1000 + (initialLT % (DEFAULT_UNDERLYING_LT - 999)); - // newLT = 1000 + (newLT % (DEFAULT_UNDERLYING_LT - 999)); - // duration = 3600 + (duration % (3600 * 24 * 90 - 3600)); - - // timestampCheck = block.timestamp + (timestampCheck % (duration + 1)); - - // address usdc = tokenTestSuite.addressOf(Tokens.USDC); - - // uint256 timestampStart = block.timestamp; - - // vm.startPrank(CONFIGURATOR); - // creditManager.setCollateralTokenData(usdc, initialLT); - // creditManager.rampLiquidationThreshold(usdc, newLT, uint40(block.timestamp), duration); - - // assertEq(creditManager.liquidationThresholds(usdc), initialLT, "LT at ramping start incorrect"); - - // uint16 expectedLT; - // if (newLT >= initialLT) { - // expectedLT = uint16( - // uint256(initialLT) - // + (uint256(newLT - initialLT) * (timestampCheck - timestampStart)) / uint256(duration) - // ); - // } else { - // expectedLT = uint16( - // uint256(initialLT) - // - (uint256(initialLT - newLT) * (timestampCheck - timestampStart)) / uint256(duration) - // ); - // } - - // vm.warp(timestampCheck); - // uint16 actualLT = creditManager.liquidationThresholds(usdc); - // uint16 diff = actualLT > expectedLT ? actualLT - expectedLT : expectedLT - actualLT; - - // assertLe(diff, 1, "LT off by more than 1"); - - // vm.warp(timestampStart + duration + 1); - - // assertEq(creditManager.liquidationThresholds(usdc), newLT, "LT at ramping end incorrect"); - } -} diff --git a/contracts/test/unit/credit/CreditManager_Quotas.t.sol b/contracts/test/integration/credit/CreditManager_Quotas.int.t.sol similarity index 80% rename from contracts/test/unit/credit/CreditManager_Quotas.t.sol rename to contracts/test/integration/credit/CreditManager_Quotas.int.t.sol index a50108fb..d2a9f551 100644 --- a/contracts/test/unit/credit/CreditManager_Quotas.t.sol +++ b/contracts/test/integration/credit/CreditManager_Quotas.int.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; -import {IAddressProvider} from "@gearbox-protocol/core-v2/contracts/interfaces/IAddressProvider.sol"; +import "../../../interfaces/IAddressProviderV3.sol"; import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; import {AccountFactory} from "@gearbox-protocol/core-v2/contracts/core/AccountFactory.sol"; @@ -13,7 +13,9 @@ import { ICreditManagerV3Events, ClosureAction, CollateralTokenData, - ManageDebtAction + ManageDebtAction, + CollateralCalcTask, + CollateralDebtData } from "../../../interfaces/ICreditManagerV3.sol"; import {IPoolQuotaKeeper, AccountQuota} from "../../../interfaces/IPoolQuotaKeeper.sol"; import {IPriceOracleV2, IPriceOracleV2Ext} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; @@ -35,7 +37,7 @@ import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; // MOCKS import {PriceFeedMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/oracles/PriceFeedMock.sol"; -import {PoolServiceMock} from "../../mocks/pool/PoolServiceMock.sol"; +import {PoolMock} from "../../mocks//pool/PoolMock.sol"; import {PoolQuotaKeeper} from "../../../pool/PoolQuotaKeeper.sol"; import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/adapters/TargetContractMock.sol"; @@ -43,7 +45,7 @@ import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; import {Tokens} from "../../config/Tokens.sol"; import {CreditManagerTestSuite} from "../../suites/CreditManagerTestSuite.sol"; -import {CreditManagerTestInternal} from "../../mocks/credit/CreditManagerTestInternal.sol"; +import {CreditManagerV3Harness} from "../../unit/credit/CreditManagerV3Harness.sol"; import {CreditConfig} from "../../config/CreditConfig.sol"; @@ -56,12 +58,12 @@ import "forge-std/console.sol"; contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper { CreditManagerTestSuite cms; - IAddressProvider addressProvider; + IAddressProviderV3 addressProvider; IWETH wethToken; AccountFactory af; CreditManagerV3 creditManager; - PoolServiceMock poolMock; + PoolMock poolMock; PoolQuotaKeeper poolQuotaKeeper; IPriceOracleV2 priceOracle; ACL acl; @@ -93,7 +95,7 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditManager = cms.creditManager(); - priceOracle = creditManager.priceOracle(); + priceOracle = IPriceOracleV2(creditManager.priceOracle()); underlying = creditManager.underlying(); } @@ -129,31 +131,31 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper /// /// - /// @dev [CMQ-1]: constructor correctly sets supportsQuotas based on pool - function test_CMQ_01_constructor_correctly_sets_quota_related_params() public { + /// @dev I:[CMQ-1]: constructor correctly sets supportsQuotas based on pool + function test_I_CMQ_01_constructor_correctly_sets_quota_related_params() public { assertTrue(creditManager.supportsQuotas(), "Credit Manager does not support quotas"); } - /// @dev [CMQ-2]: setQuotedMask works correctly - function test_CMQ_02_setQuotedMask_works_correctly() public { + /// @dev I:[CMQ-2]: setQuotedMask works correctly + function test_I_CMQ_02_setQuotedMask_works_correctly() public { _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); uint256 usdcMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)); uint256 linkMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - uint256 quotedTokenMask = creditManager.quotedTokenMask(); + uint256 quotedTokensMask = creditManager.quotedTokensMask(); vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setQuotedMask(quotedTokenMask | usdcMask); + creditManager.setQuotedMask(quotedTokensMask | usdcMask); vm.prank(CONFIGURATOR); - creditManager.setQuotedMask(quotedTokenMask | usdcMask); + creditManager.setQuotedMask(quotedTokensMask | usdcMask); - assertEq(creditManager.quotedTokenMask(), usdcMask | linkMask, "New limited mask is incorrect"); + assertEq(creditManager.quotedTokensMask(), usdcMask | linkMask, "New limited mask is incorrect"); } - /// @dev [CMQ-3]: updateQuotas works correctly - function test_CMQ_03_updateQuotas_works_correctly() public { + /// @dev I:[CMQ-3]: updateQuotas works correctly + function test_I_CMQ_03_updateQuotas_works_correctly() public { _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 500, uint96(1_000_000 * WAD)); @@ -163,13 +165,12 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper assertEq(cumulativeQuotaInterest, 1, "SETUP: Cumulative quota interest was not updated correctly"); - vm.expectRevert(CallerNotCreditFacadeException.selector); - vm.prank(FRIEND); - creditManager.updateQuota({ - creditAccount: creditAccount, - token: tokenTestSuite.addressOf(Tokens.LINK), - quotaChange: 100_000 - }); + { + address link = tokenTestSuite.addressOf(Tokens.LINK); + vm.expectRevert(CallerNotCreditFacadeException.selector); + vm.prank(FRIEND); + creditManager.updateQuota({creditAccount: creditAccount, token: link, quotaChange: 100_000}); + } vm.expectCall( address(poolQuotaKeeper), @@ -203,20 +204,19 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper assertEq( cumulativeQuotaInterest, - (100000 * 1000 + 200000 * 500) / PERCENTAGE_FACTOR + 1, + (100000 * 1000) / PERCENTAGE_FACTOR + 1, "Cumulative quota interest was not updated correctly" ); - vm.expectRevert(TokenIsNotQuotedException.selector); - creditManager.updateQuota({ - creditAccount: creditAccount, - token: tokenTestSuite.addressOf(Tokens.USDC), - quotaChange: 100_000 - }); + { + address usdc = tokenTestSuite.addressOf(Tokens.USDC); + vm.expectRevert(TokenIsNotQuotedException.selector); + creditManager.updateQuota({creditAccount: creditAccount, token: usdc, quotaChange: 100_000}); + } } - /// @dev [CMQ-4]: Quotas are handled correctly on debt decrease: amount < quota interest case - function test_CMQ_04_quotas_are_handled_correctly_at_repayment_partial_case() public { + /// @dev I:[CMQ-4]: Quotas are handled correctly on debt decrease: amount < quota interest case + function test_I_CMQ_04_quotas_are_handled_correctly_at_repayment_partial_case() public { // _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); // _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 500, uint96(1_000_000 * WAD)); @@ -250,22 +250,22 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper // uint256 expectedQuotaInterestRepaid = (amountRepaid * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR + feeInterest); // (, uint256 totalDebtBefore, uint256 totalDebtBeforeFee) = - // creditManager.calcCreditAccountAccruedInterest(creditAccount); + // creditManager.calcAccruedInterestAndFees(creditAccount); // creditManager.manageDebt( // creditAccount, amountRepaid, tokensToEnable | UNDERLYING_TOKEN_MASK, ManageDebtAction.DECREASE_DEBT // ); // (, uint256 totalDebtAfter, uint256 totalDebtAfterFee) = - // creditManager.calcCreditAccountAccruedInterest(creditAccount); + // creditManager.calcAccruedInterestAndFees(creditAccount); // assertEq(totalDebtAfter, totalDebtBefore - expectedQuotaInterestRepaid, "Debt updated incorrectly"); // assertEq(totalDebtAfterFee, totalDebtBeforeFee - amountRepaid, "Debt updated incorrectly"); } - /// @dev [CMQ-5]: Quotas are handled correctly on debt decrease: amount >= quota interest case - function test_CMQ_05_quotas_are_handled_correctly_at_repayment_full_case() public { + /// @dev I:[CMQ-5]: Quotas are handled correctly on debt decrease: amount >= quota interest case + function test_I_CMQ_05_quotas_are_handled_correctly_at_repayment_full_case() public { // _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 1000, uint96(1_000_000 * WAD)); // _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 500, uint96(1_000_000 * WAD)); @@ -292,7 +292,7 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper // uint256 amountRepaid = 35 * WAD; - // (,, uint256 totalDebtBefore) = creditManager.calcCreditAccountAccruedInterest(creditAccount); + // (,, uint256 totalDebtBefore) = creditManager.calcAccruedInterestAndFees(creditAccount); // creditManager.manageDebt( // creditAccount, amountRepaid, tokensToEnable | UNDERLYING_TOKEN_MASK, ManageDebtAction.DECREASE_DEBT @@ -302,13 +302,13 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper // assertEq(cumulativeQuotaInterest, 1, "Cumulative quota interest was not updated correctly"); - // (,, uint256 totalDebtAfter) = creditManager.calcCreditAccountAccruedInterest(creditAccount); + // (,, uint256 totalDebtAfter) = creditManager.calcAccruedInterestAndFees(creditAccount); // assertEq(totalDebtAfter, totalDebtBefore - amountRepaid, "Debt updated incorrectly"); } - /// @dev [CMQ-6]: Quotas are disabled on closing an account - function test_CMQ_06_quotas_are_disabled_on_close_account_and_all_quota_fees_are_repaid() public { + /// @dev I:[CMQ-6]: Quotas are disabled on closing an account + function test_I_CMQ_06_quotas_are_disabled_on_close_account_and_all_quota_fees_are_repaid() public { _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 5_00, uint96(1_000_000 * WAD)); @@ -352,29 +352,15 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(poolMock)); - /// address borrower, - // ClosureAction closureActionType, - // uint256 totalValue, - // address payer, - // address to, - // uint256 enabledTokensMask, - // uint256 skipTokensMask, - // uint256 borrowedAmountWithInterest, - // bool convertWETH - - // (, uint256 borrowedAmountWithInterest,) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.CLOSE_ACCOUNT, - // 0, - // USER, - // USER, - // tokensToEnable | UNDERLYING_TOKEN_MASK, - // 0, - // borrowedAmountWithInterest, - // false - // ); + creditManager.closeCreditAccount( + creditAccount, + ClosureAction.CLOSE_ACCOUNT, + creditManager.calcDebtAndCollateral(creditAccount, CollateralCalcTask.DEBT_ONLY), + USER, + USER, + 0, + false + ); expectBalance( Tokens.DAI, @@ -387,15 +373,13 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper poolQuotaKeeper.getQuota(creditAccount, tokenTestSuite.addressOf(Tokens.LINK)); assertEq(uint256(quota), 1, "Quota was not set to 0"); - assertEq(uint256(cumulativeIndexLU), 0, "Cumulative index was not updated"); (quota, cumulativeIndexLU) = poolQuotaKeeper.getQuota(creditAccount, tokenTestSuite.addressOf(Tokens.USDT)); assertEq(uint256(quota), 1, "Quota was not set to 0"); - assertEq(uint256(cumulativeIndexLU), 0, "Cumulative index was not updated"); } - // /// @dev [CMQ-7] enableToken, disableToken and changeEnabledTokens do nothing for limited tokens - // function test_CMQ_07_enable_disable_changeEnabled_do_nothing_for_limited_tokens() public { + // /// @dev I:[CMQ-7] enableToken, disableToken and changeEnabledTokens do nothing for limited tokens + // function test_I_CMQ_07_enable_disable_changeEnabled_do_nothing_for_limited_tokens() public { // _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); // (,,, address creditAccount) = cms.openCreditAccount(); // creditManager.transferAccountOwnership(USER, address(this)); @@ -419,8 +403,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper // expectTokenIsEnabled(creditAccount, Tokens.LINK, true); // } - /// @dev [CMQ-8]: fullCollateralCheck fuzzing test with quotas - function test_CMQ_08_fullCollateralCheck_fuzzing_test_quotas( + /// @dev I:[CMQ-8]: fullCollateralCheck fuzzing test with quotas + function test_I_CMQ_08_fullCollateralCheck_fuzzing_test_quotas( uint128 borrowedAmount, uint128 daiBalance, uint128 usdcBalance, @@ -481,7 +465,7 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper // enabledTokensMap |= creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); // } - // CreditManagerTestInternal(address(creditManager)).setenabledTokensMask(creditAccount, enabledTokensMap); + // CreditManagerV3Harness(address(creditManager)).setenabledTokensMask(creditAccount, enabledTokensMap); // tokenTestSuite.mint(Tokens.WETH, creditAccount, wethBalance); // tokenTestSuite.mint(Tokens.USDC, creditAccount, usdcBalance); @@ -525,7 +509,7 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper // * creditConfig.lt(Tokens.WETH) // ) / WAD; - // (,, uint256 borrowedAmountWithInterestAndFees) = creditManager.calcCreditAccountAccruedInterest(creditAccount); + // (,, uint256 borrowedAmountWithInterestAndFees) = creditManager.calcAccruedInterestAndFees(creditAccount); // uint256 debtUSD = (borrowedAmountWithInterestAndFees * minHealthFactor * tokenTestSuite.prices(Tokens.DAI)) // / PERCENTAGE_FACTOR / WAD; @@ -541,8 +525,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper // creditManager.fullCollateralCheck(creditAccount, enabledTokensMap, new uint256[](0), minHealthFactor); } - /// @dev [CMQ-9]: fullCollateralCheck does not check non-limited tokens if limited are enough to cover debt - function test_CMQ_09_fullCollateralCheck_skips_normal_tokens_if_limited_tokens_cover_debt() public { + /// @dev I:[CMQ-9]: fullCollateralCheck does not check non-limited tokens if limited are enough to cover debt + function test_I_CMQ_09_fullCollateralCheck_skips_normal_tokens_if_limited_tokens_cover_debt() public { _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDC), 500, uint96(1_000_000 * WAD)); @@ -588,8 +572,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditManager.fullCollateralCheck(creditAccount, enableTokenMask, collateralHints, 10000); } - /// @dev [CMQ-10]: calcCreditAccountAccruedInterest correctly counts quota interest - function test_CMQ_10_calcCreditAccountAccruedInterest_correctly_includes_quota_interest( + /// @dev I:[CMQ-10]: calcAccruedInterestAndFees correctly counts quota interest + function test_I_CMQ_10_calcAccruedInterestAndFees_correctly_includes_quota_interest( uint96 quotaLink, uint96 quotaUsdt ) public { @@ -621,11 +605,11 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper // uint256 enabledTokensMap = tokensToEnable | UNDERLYING_TOKEN_MASK; - // CreditManagerTestInternal(address(creditManager)).setenabledTokensMask(creditAccount, enabledTokensMap); + // CreditManagerV3Harness(address(creditManager)).setenabledTokensMask(creditAccount, enabledTokensMap); // vm.warp(block.timestamp + 60 * 60 * 24 * 365); - // (,, uint256 totalDebt) = creditManager.calcCreditAccountAccruedInterest(creditAccount); + // (,, uint256 totalDebt) = creditManager.calcAccruedInterestAndFees(creditAccount); // uint256 expectedTotalDebt = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate; // expectedTotalDebt += (quotaLink * 1000 + quotaUsdt * 500) / PERCENTAGE_FACTOR; @@ -640,11 +624,11 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper } // [DEPRECIATED]: We test that after full collateral - // /// @dev [CMQ-11]: updateQuotas reverts on too many enabled tokens - // function test_CMQ_11_updateQuotas_reverts_on_too_many_tokens_enabled() public { + // /// @dev I:[CMQ-11]: updateQuotas reverts on too many enabled tokens + // function test_I_CMQ_11_updateQuotas_reverts_on_too_many_tokens_enabled() public { // (,,, address creditAccount) = cms.openCreditAccount(); - // uint256 maxTokens = creditManager.maxAllowedEnabledTokenLength(); + // uint256 maxTokens = creditManager.maxEnabledTokens(); // QuotaUpdate[] memory quotaUpdates = _addManyLimitedTokens(maxTokens + 1, 100); @@ -652,13 +636,15 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper // creditManager.updateQuotas(creditAccount, quotaUpdates); // } - /// @dev [CMQ-12]: Credit Manager zeroes limits on quoted tokens upon incurring a loss - function test_CMQ_12_creditManager_triggers_limit_zeroing_on_loss() public { + /// @dev I:[CMQ-12]: Credit Manager zeroes limits on quoted tokens upon incurring a loss + function test_I_CMQ_12_creditManager_triggers_limit_zeroing_on_loss() public { _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 500, uint96(1_000_000 * WAD)); (,,, address creditAccount) = cms.openCreditAccount(); + tokenTestSuite.mint(Tokens.DAI, creditAccount, DAI_ACCOUNT_AMOUNT * 2); + uint256 enabledTokensMap = UNDERLYING_TOKEN_MASK; (uint256 tokensToEnable,) = creditManager.updateQuota({ @@ -675,24 +661,22 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper }); enabledTokensMap |= tokensToEnable; - address[] memory quotedTokens = new address[](creditManager.maxAllowedEnabledTokenLength() + 1); + creditManager.fullCollateralCheck(creditAccount, enabledTokensMap, new uint256[](0), 10_000); - quotedTokens[0] = tokenTestSuite.addressOf(Tokens.LINK); - quotedTokens[1] = tokenTestSuite.addressOf(Tokens.USDT); + address[] memory quotedTokens = new address[](creditManager.maxEnabledTokens()); - // vm.expectCall(address(poolQuotaKeeper), abi.encodeCall(IPoolQuotaKeeper.setLimitsToZero, (quotedTokens))); + quotedTokens[0] = tokenTestSuite.addressOf(Tokens.USDT); + quotedTokens[1] = tokenTestSuite.addressOf(Tokens.LINK); - // creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.LIQUIDATE_ACCOUNT, - // DAI_ACCOUNT_AMOUNT, - // USER, - // USER, - // enabledTokensMap, - // 0, - // DAI_ACCOUNT_AMOUNT, - // false - // ); + CollateralDebtData memory cdd = + creditManager.calcDebtAndCollateral(creditAccount, CollateralCalcTask.DEBT_COLLATERAL_CANCEL_WITHDRAWALS); + cdd.totalValue = 0; + + vm.expectCall( + address(poolQuotaKeeper), abi.encodeCall(IPoolQuotaKeeper.removeQuotas, (creditAccount, quotedTokens, true)) + ); + + creditManager.closeCreditAccount(creditAccount, ClosureAction.LIQUIDATE_ACCOUNT, cdd, USER, USER, 0, false); for (uint256 i = 0; i < quotedTokens.length; ++i) { if (quotedTokens[i] == address(0)) continue; diff --git a/contracts/test/integration/credit/CreditManager_Quotas.t.sol b/contracts/test/integration/credit/CreditManager_Quotas.t.sol deleted file mode 100644 index a50108fb..00000000 --- a/contracts/test/integration/credit/CreditManager_Quotas.t.sol +++ /dev/null @@ -1,705 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; - -import {IAddressProvider} from "@gearbox-protocol/core-v2/contracts/interfaces/IAddressProvider.sol"; -import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; - -import {AccountFactory} from "@gearbox-protocol/core-v2/contracts/core/AccountFactory.sol"; -import {ICreditAccount} from "@gearbox-protocol/core-v2/contracts/interfaces/ICreditAccount.sol"; -import { - ICreditManagerV3, - ICreditManagerV3Events, - ClosureAction, - CollateralTokenData, - ManageDebtAction -} from "../../../interfaces/ICreditManagerV3.sol"; -import {IPoolQuotaKeeper, AccountQuota} from "../../../interfaces/IPoolQuotaKeeper.sol"; -import {IPriceOracleV2, IPriceOracleV2Ext} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; - -import {CreditManagerV3} from "../../../credit/CreditManagerV3.sol"; -import {UNDERLYING_TOKEN_MASK} from "../../../libraries/BitMask.sol"; - -import {IPoolService} from "@gearbox-protocol/core-v2/contracts/interfaces/IPoolService.sol"; - -import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ERC20Mock} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/ERC20Mock.sol"; -import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; - -// TESTS -import "../../lib/constants.sol"; - -import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; - -// MOCKS -import {PriceFeedMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/oracles/PriceFeedMock.sol"; -import {PoolServiceMock} from "../../mocks/pool/PoolServiceMock.sol"; -import {PoolQuotaKeeper} from "../../../pool/PoolQuotaKeeper.sol"; -import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/adapters/TargetContractMock.sol"; - -// SUITES -import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; -import {Tokens} from "../../config/Tokens.sol"; -import {CreditManagerTestSuite} from "../../suites/CreditManagerTestSuite.sol"; -import {CreditManagerTestInternal} from "../../mocks/credit/CreditManagerTestInternal.sol"; - -import {CreditConfig} from "../../config/CreditConfig.sol"; - -// EXCEPTIONS -import "../../../interfaces/IExceptions.sol"; - -import {Test} from "forge-std/Test.sol"; -import "forge-std/console.sol"; - -contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper { - CreditManagerTestSuite cms; - - IAddressProvider addressProvider; - IWETH wethToken; - - AccountFactory af; - CreditManagerV3 creditManager; - PoolServiceMock poolMock; - PoolQuotaKeeper poolQuotaKeeper; - IPriceOracleV2 priceOracle; - ACL acl; - address underlying; - - CreditConfig creditConfig; - - function setUp() public { - tokenTestSuite = new TokensTestSuite(); - - tokenTestSuite.topUpWETH{value: 100 * WAD}(); - _connectCreditManagerSuite(Tokens.DAI, false); - } - - /// - /// HELPERS - - function _connectCreditManagerSuite(Tokens t, bool internalSuite) internal { - creditConfig = new CreditConfig(tokenTestSuite, t); - cms = new CreditManagerTestSuite(creditConfig, internalSuite, true, 1); - - acl = cms.acl(); - - addressProvider = cms.addressProvider(); - af = cms.af(); - - poolMock = cms.poolMock(); - poolQuotaKeeper = cms.poolQuotaKeeper(); - - creditManager = cms.creditManager(); - - priceOracle = creditManager.priceOracle(); - underlying = creditManager.underlying(); - } - - function _addQuotedToken(address token, uint16 rate, uint96 limit) internal { - cms.makeTokenQuoted(token, rate, limit); - } - - // function _addManyLimitedTokens(uint256 numTokens, uint96 quota) - // internal - // returns (QuotaUpdate[] memory quotaChanges) - // { - // quotaChanges = new QuotaUpdate[](numTokens); - - // for (uint256 i = 0; i < numTokens; i++) { - // ERC20Mock t = new ERC20Mock("new token", "nt", 18); - // PriceFeedMock pf = new PriceFeedMock(10**8, 8); - - // vm.startPrank(CONFIGURATOR); - // creditManager.addToken(address(t)); - // IPriceOracleV2Ext(address(priceOracle)).addPriceFeed(address(t), address(pf)); - // creditManager.setCollateralTokenData(address(t), 8000, 8000, type(uint40).max, 0); - // vm.stopPrank(); - - // _addQuotedToken(address(t), 100, type(uint96).max); - - // quotaChanges[i] = QuotaUpdate({token: address(t), quotaChange: int96(quota)}); - // } - // } - - /// - /// - /// TESTS - /// - /// - - /// @dev [CMQ-1]: constructor correctly sets supportsQuotas based on pool - function test_CMQ_01_constructor_correctly_sets_quota_related_params() public { - assertTrue(creditManager.supportsQuotas(), "Credit Manager does not support quotas"); - } - - /// @dev [CMQ-2]: setQuotedMask works correctly - function test_CMQ_02_setQuotedMask_works_correctly() public { - _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); - - uint256 usdcMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)); - uint256 linkMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - - uint256 quotedTokenMask = creditManager.quotedTokenMask(); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setQuotedMask(quotedTokenMask | usdcMask); - - vm.prank(CONFIGURATOR); - creditManager.setQuotedMask(quotedTokenMask | usdcMask); - - assertEq(creditManager.quotedTokenMask(), usdcMask | linkMask, "New limited mask is incorrect"); - } - - /// @dev [CMQ-3]: updateQuotas works correctly - function test_CMQ_03_updateQuotas_works_correctly() public { - _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); - _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 500, uint96(1_000_000 * WAD)); - - (,,, address creditAccount) = cms.openCreditAccount(); - - (,, uint256 cumulativeQuotaInterest,,,) = creditManager.creditAccountInfo(creditAccount); - - assertEq(cumulativeQuotaInterest, 1, "SETUP: Cumulative quota interest was not updated correctly"); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - vm.prank(FRIEND); - creditManager.updateQuota({ - creditAccount: creditAccount, - token: tokenTestSuite.addressOf(Tokens.LINK), - quotaChange: 100_000 - }); - - vm.expectCall( - address(poolQuotaKeeper), - abi.encodeCall( - IPoolQuotaKeeper.updateQuota, (creditAccount, tokenTestSuite.addressOf(Tokens.LINK), 100_000) - ) - ); - - uint256 linkMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - - (uint256 tokensToEnable, uint256 tokensToDisable) = creditManager.updateQuota({ - creditAccount: creditAccount, - token: tokenTestSuite.addressOf(Tokens.LINK), - quotaChange: 100_000 - }); - - assertEq(tokensToEnable, linkMask, "Incorrect tokensToEnble"); - assertEq(tokensToDisable, 0, "Incorrect tokensToDisable"); - - vm.warp(block.timestamp + 365 days); - - (tokensToEnable, tokensToDisable) = creditManager.updateQuota({ - creditAccount: creditAccount, - token: tokenTestSuite.addressOf(Tokens.LINK), - quotaChange: -100_000 - }); - assertEq(tokensToEnable, 0, "Incorrect tokensToEnable"); - assertEq(tokensToDisable, linkMask, "Incorrect tokensToDisable"); - - (,, cumulativeQuotaInterest,,,) = creditManager.creditAccountInfo(creditAccount); - - assertEq( - cumulativeQuotaInterest, - (100000 * 1000 + 200000 * 500) / PERCENTAGE_FACTOR + 1, - "Cumulative quota interest was not updated correctly" - ); - - vm.expectRevert(TokenIsNotQuotedException.selector); - creditManager.updateQuota({ - creditAccount: creditAccount, - token: tokenTestSuite.addressOf(Tokens.USDC), - quotaChange: 100_000 - }); - } - - /// @dev [CMQ-4]: Quotas are handled correctly on debt decrease: amount < quota interest case - function test_CMQ_04_quotas_are_handled_correctly_at_repayment_partial_case() public { - // _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); - // _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 500, uint96(1_000_000 * WAD)); - - // (,,, address creditAccount) = cms.openCreditAccount(); - // tokenTestSuite.mint(Tokens.DAI, creditAccount, DAI_ACCOUNT_AMOUNT * 2); - - // QuotaUpdate[] memory quotaUpdates = new QuotaUpdate[](2); - // quotaUpdates[0] = - // QuotaUpdate({token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: int96(uint96(100 * WAD))}); - // quotaUpdates[1] = - // QuotaUpdate({token: tokenTestSuite.addressOf(Tokens.USDT), quotaChange: int96(uint96(200 * WAD))}); - - // vm.expectCall( - // address(poolQuotaKeeper), abi.encodeCall(IPoolQuotaKeeper.updateQuotas, (creditAccount, quotaUpdates)) - // ); - - // (uint256 tokensToEnable,) = creditManager.updateQuotas(creditAccount, quotaUpdates); - - // /// We use fullCollateralCheck to update enabledTokensMask - - // creditManager.fullCollateralCheck( - // creditAccount, tokensToEnable | UNDERLYING_TOKEN_MASK, new uint256[](0), 10_000 - // ); - - // vm.warp(block.timestamp + 365 days); - - // (uint16 feeInterest,,,,) = creditManager.fees(); - - // uint256 amountRepaid = (PERCENTAGE_FACTOR + feeInterest) * WAD / 1_000; - - // uint256 expectedQuotaInterestRepaid = (amountRepaid * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR + feeInterest); - - // (, uint256 totalDebtBefore, uint256 totalDebtBeforeFee) = - // creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // creditManager.manageDebt( - // creditAccount, amountRepaid, tokensToEnable | UNDERLYING_TOKEN_MASK, ManageDebtAction.DECREASE_DEBT - // ); - - // (, uint256 totalDebtAfter, uint256 totalDebtAfterFee) = - // creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // assertEq(totalDebtAfter, totalDebtBefore - expectedQuotaInterestRepaid, "Debt updated incorrectly"); - - // assertEq(totalDebtAfterFee, totalDebtBeforeFee - amountRepaid, "Debt updated incorrectly"); - } - - /// @dev [CMQ-5]: Quotas are handled correctly on debt decrease: amount >= quota interest case - function test_CMQ_05_quotas_are_handled_correctly_at_repayment_full_case() public { - // _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 1000, uint96(1_000_000 * WAD)); - // _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 500, uint96(1_000_000 * WAD)); - - // (,,, address creditAccount) = cms.openCreditAccount(); - // tokenTestSuite.mint(Tokens.DAI, creditAccount, DAI_ACCOUNT_AMOUNT * 2); - - // QuotaUpdate[] memory quotaUpdates = new QuotaUpdate[](2); - // quotaUpdates[0] = - // QuotaUpdate({token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: int96(uint96(100 * WAD))}); - // quotaUpdates[1] = - // QuotaUpdate({token: tokenTestSuite.addressOf(Tokens.USDT), quotaChange: int96(uint96(200 * WAD))}); - - // vm.expectCall( - // address(poolQuotaKeeper), abi.encodeCall(IPoolQuotaKeeper.updateQuotas, (creditAccount, quotaUpdates)) - // ); - - // (uint256 tokensToEnable,) = creditManager.updateQuotas(creditAccount, quotaUpdates); - - // creditManager.fullCollateralCheck( - // creditAccount, tokensToEnable | UNDERLYING_TOKEN_MASK, new uint256[](0), 10_000 - // ); - - // vm.warp(block.timestamp + 365 days); - - // uint256 amountRepaid = 35 * WAD; - - // (,, uint256 totalDebtBefore) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // creditManager.manageDebt( - // creditAccount, amountRepaid, tokensToEnable | UNDERLYING_TOKEN_MASK, ManageDebtAction.DECREASE_DEBT - // ); - - // (,, uint256 cumulativeQuotaInterest,,,) = creditManager.creditAccountInfo(creditAccount); - - // assertEq(cumulativeQuotaInterest, 1, "Cumulative quota interest was not updated correctly"); - - // (,, uint256 totalDebtAfter) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // assertEq(totalDebtAfter, totalDebtBefore - amountRepaid, "Debt updated incorrectly"); - } - - /// @dev [CMQ-6]: Quotas are disabled on closing an account - function test_CMQ_06_quotas_are_disabled_on_close_account_and_all_quota_fees_are_repaid() public { - _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); - _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 5_00, uint96(1_000_000 * WAD)); - - ( - uint256 borrowedAmount, - uint256 cumulativeIndexLastUpdate, - uint256 cumulativeIndexAtClose, - address creditAccount - ) = cms.openCreditAccount(); - - tokenTestSuite.mint(Tokens.DAI, creditAccount, DAI_ACCOUNT_AMOUNT * 2); - - uint256 enabledTokensMask = UNDERLYING_TOKEN_MASK; - (uint256 tokensToEnable,) = creditManager.updateQuota({ - creditAccount: creditAccount, - token: tokenTestSuite.addressOf(Tokens.LINK), - quotaChange: int96(uint96(100 * WAD)) - }); - enabledTokensMask |= tokensToEnable; - - (tokensToEnable,) = creditManager.updateQuota({ - creditAccount: creditAccount, - token: tokenTestSuite.addressOf(Tokens.USDT), - quotaChange: int96(uint96(200 * WAD)) - }); - enabledTokensMask |= tokensToEnable; - - creditManager.fullCollateralCheck(creditAccount, enabledTokensMask, new uint256[](0), 10_000); - - (uint16 feeInterest,,,,) = creditManager.fees(); - - uint256 interestAccured = (borrowedAmount * cumulativeIndexAtClose / cumulativeIndexLastUpdate - borrowedAmount) - * (PERCENTAGE_FACTOR + feeInterest) / PERCENTAGE_FACTOR; - - uint256 expectedQuotasInterest = (100 * WAD * 10_00 / PERCENTAGE_FACTOR + 200 * WAD * 5_00 / PERCENTAGE_FACTOR) - * (PERCENTAGE_FACTOR + feeInterest) / PERCENTAGE_FACTOR; - - vm.warp(block.timestamp + 365 days); - - tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); - - uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(poolMock)); - - /// address borrower, - // ClosureAction closureActionType, - // uint256 totalValue, - // address payer, - // address to, - // uint256 enabledTokensMask, - // uint256 skipTokensMask, - // uint256 borrowedAmountWithInterest, - // bool convertWETH - - // (, uint256 borrowedAmountWithInterest,) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.CLOSE_ACCOUNT, - // 0, - // USER, - // USER, - // tokensToEnable | UNDERLYING_TOKEN_MASK, - // 0, - // borrowedAmountWithInterest, - // false - // ); - - expectBalance( - Tokens.DAI, - address(poolMock), - poolBalanceBefore + borrowedAmount + interestAccured + expectedQuotasInterest, - "Incorrect pool balance" - ); - - (uint96 quota, uint192 cumulativeIndexLU) = - poolQuotaKeeper.getQuota(creditAccount, tokenTestSuite.addressOf(Tokens.LINK)); - - assertEq(uint256(quota), 1, "Quota was not set to 0"); - assertEq(uint256(cumulativeIndexLU), 0, "Cumulative index was not updated"); - - (quota, cumulativeIndexLU) = poolQuotaKeeper.getQuota(creditAccount, tokenTestSuite.addressOf(Tokens.USDT)); - assertEq(uint256(quota), 1, "Quota was not set to 0"); - assertEq(uint256(cumulativeIndexLU), 0, "Cumulative index was not updated"); - } - - // /// @dev [CMQ-7] enableToken, disableToken and changeEnabledTokens do nothing for limited tokens - // function test_CMQ_07_enable_disable_changeEnabled_do_nothing_for_limited_tokens() public { - // _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); - // (,,, address creditAccount) = cms.openCreditAccount(); - // creditManager.transferAccountOwnership(USER, address(this)); - - // // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.LINK)); - // expectTokenIsEnabled(creditAccount, Tokens.LINK, false); - - // creditManager.changeEnabledTokens(creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)), 0); - // expectTokenIsEnabled(creditAccount, Tokens.LINK, false); - - // QuotaUpdate[] memory quotaUpdates = new QuotaUpdate[](1); - // quotaUpdates[0] = - // QuotaUpdate({token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: int96(uint96(100 * WAD))}); - - // creditManager.updateQuotas(creditAccount, quotaUpdates); - - // creditManager.disableToken(tokenTestSuite.addressOf(Tokens.LINK)); - // expectTokenIsEnabled(creditAccount, Tokens.LINK, true); - - // creditManager.changeEnabledTokens(0, creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK))); - // expectTokenIsEnabled(creditAccount, Tokens.LINK, true); - // } - - /// @dev [CMQ-8]: fullCollateralCheck fuzzing test with quotas - function test_CMQ_08_fullCollateralCheck_fuzzing_test_quotas( - uint128 borrowedAmount, - uint128 daiBalance, - uint128 usdcBalance, - uint128 linkBalance, - uint128 wethBalance, - uint96 usdcQuota, - uint96 linkQuota, - bool enableWETH, - uint16 minHealthFactor - ) public { - // _connectCreditManagerSuite(Tokens.DAI, true); - - // _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); - // _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDC), 500, uint96(1_000_000 * WAD)); - - // vm.assume(borrowedAmount > WAD); - // vm.assume(usdcQuota < type(uint96).max / 2); - // vm.assume(linkQuota < type(uint96).max / 2); - // vm.assume(minHealthFactor >= 10_000); - - // console.log("ba", borrowedAmount); - // // uint128 daiBalance, - // // uint128 usdcBalance, - // // uint128 linkBalance, - // // uint128 wethBalance, - // // uint96 usdcQuota, - // // uint96 linkQuota, - // // bool enableWETH, - // // uint16 minHealthFactor) - - // tokenTestSuite.mint(Tokens.DAI, address(poolMock), borrowedAmount); - - // (,,, address creditAccount) = cms.openCreditAccount(borrowedAmount); - // creditManager.transferAccountOwnership(creditAccount, address(this)); - - // if (daiBalance > borrowedAmount) { - // tokenTestSuite.mint(Tokens.DAI, creditAccount, daiBalance - borrowedAmount); - // } else { - // tokenTestSuite.burn(Tokens.DAI, creditAccount, borrowedAmount - daiBalance); - // } - - // expectBalance(Tokens.DAI, creditAccount, daiBalance); - - // uint256 tokensToEnable; - - // { - // QuotaUpdate[] memory quotaUpdates = new QuotaUpdate[](2); - // quotaUpdates[0] = - // QuotaUpdate({token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: int96(uint96(linkQuota))}); - // quotaUpdates[1] = - // QuotaUpdate({token: tokenTestSuite.addressOf(Tokens.USDC), quotaChange: int96(uint96(usdcQuota))}); - - // (tokensToEnable,) = creditManager.updateQuotas(creditAccount, quotaUpdates); - // } - - // uint256 enabledTokensMap = tokensToEnable | UNDERLYING_TOKEN_MASK; - // if (enableWETH) { - // enabledTokensMap |= creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - // } - - // CreditManagerTestInternal(address(creditManager)).setenabledTokensMask(creditAccount, enabledTokensMap); - - // tokenTestSuite.mint(Tokens.WETH, creditAccount, wethBalance); - // tokenTestSuite.mint(Tokens.USDC, creditAccount, usdcBalance); - // tokenTestSuite.mint(Tokens.LINK, creditAccount, linkBalance); - - // uint256 twvUSD = ( - // tokenTestSuite.balanceOf(Tokens.DAI, creditAccount) * tokenTestSuite.prices(Tokens.DAI) - // * creditConfig.lt(Tokens.DAI) - // ) / WAD; - - // { - // uint256 valueUsdc = - // (tokenTestSuite.balanceOf(Tokens.USDC, creditAccount) * tokenTestSuite.prices(Tokens.USDC)) / (10 ** 6); - - // uint256 quotaUsdc = usdcQuota > 1_000_000 * WAD ? 1_000_000 * WAD : usdcQuota; - - // quotaUsdc = (quotaUsdc * tokenTestSuite.prices(Tokens.DAI)) / WAD; - - // uint256 tvIncrease = valueUsdc < quotaUsdc ? valueUsdc : quotaUsdc; - - // twvUSD += tvIncrease * creditConfig.lt(Tokens.USDC); - // } - - // { - // uint256 valueLink = - // (tokenTestSuite.balanceOf(Tokens.LINK, creditAccount) * tokenTestSuite.prices(Tokens.LINK)) / WAD; - - // uint256 quotaLink = linkQuota > 1_000_000 * WAD ? 1_000_000 * WAD : linkQuota; - - // quotaLink = (quotaLink * tokenTestSuite.prices(Tokens.DAI)) / WAD; - - // uint256 tvIncrease = valueLink < quotaLink ? valueLink : quotaLink; - - // twvUSD += tvIncrease * creditConfig.lt(Tokens.LINK); - // } - - // twvUSD += !enableWETH - // ? 0 - // : ( - // tokenTestSuite.balanceOf(Tokens.WETH, creditAccount) * tokenTestSuite.prices(Tokens.WETH) - // * creditConfig.lt(Tokens.WETH) - // ) / WAD; - - // (,, uint256 borrowedAmountWithInterestAndFees) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // uint256 debtUSD = (borrowedAmountWithInterestAndFees * minHealthFactor * tokenTestSuite.prices(Tokens.DAI)) - // / PERCENTAGE_FACTOR / WAD; - - // twvUSD /= PERCENTAGE_FACTOR; - - // bool shouldRevert = twvUSD < debtUSD; - - // if (shouldRevert) { - // vm.expectRevert(NotEnoughCollateralException.selector); - // } - - // creditManager.fullCollateralCheck(creditAccount, enabledTokensMap, new uint256[](0), minHealthFactor); - } - - /// @dev [CMQ-9]: fullCollateralCheck does not check non-limited tokens if limited are enough to cover debt - function test_CMQ_09_fullCollateralCheck_skips_normal_tokens_if_limited_tokens_cover_debt() public { - _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); - _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDC), 500, uint96(1_000_000 * WAD)); - - tokenTestSuite.mint(Tokens.DAI, address(poolMock), 1_250_000 * WAD); - - (,,, address creditAccount) = cms.openCreditAccount(1_250_000 * WAD); - creditManager.transferAccountOwnership(creditAccount, address(this)); - - uint256 tokenToEnable; - - { - (uint256 tokenToEnable1,) = creditManager.updateQuota({ - creditAccount: creditAccount, - token: tokenTestSuite.addressOf(Tokens.LINK), - quotaChange: int96(uint96(1_000_000 * WAD)) - }); - - tokenToEnable |= tokenToEnable1; - - (tokenToEnable1,) = creditManager.updateQuota({ - creditAccount: creditAccount, - token: tokenTestSuite.addressOf(Tokens.USDC), - quotaChange: int96(uint96(1_000_000 * WAD)) - }); - tokenToEnable |= tokenToEnable1; - } - - tokenTestSuite.mint(Tokens.USDC, creditAccount, RAY); - tokenTestSuite.mint(Tokens.LINK, creditAccount, RAY); - - vm.prank(CONFIGURATOR); - creditManager.addToken(DUMB_ADDRESS); - - // creditManager.checkAndEnableToken(DUMB_ADDRESS); - - uint256 revertMask = creditManager.getTokenMaskOrRevert(DUMB_ADDRESS); - - uint256[] memory collateralHints = new uint256[](1); - collateralHints[0] = revertMask; - - uint256 enableTokenMask = tokenToEnable | revertMask | UNDERLYING_TOKEN_MASK; - - creditManager.fullCollateralCheck(creditAccount, enableTokenMask, collateralHints, 10000); - } - - /// @dev [CMQ-10]: calcCreditAccountAccruedInterest correctly counts quota interest - function test_CMQ_10_calcCreditAccountAccruedInterest_correctly_includes_quota_interest( - uint96 quotaLink, - uint96 quotaUsdt - ) public { - // _connectCreditManagerSuite(Tokens.DAI, true); - - // _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); - // _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 500, uint96(1_000_000 * WAD)); - - // (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexAtClose, address creditAccount) = - // cms.openCreditAccount(); - - // vm.assume(quotaLink < type(uint96).max / 2); - // vm.assume(quotaUsdt < type(uint96).max / 2); - - // QuotaUpdate[] memory quotaUpdates = new QuotaUpdate[](2); - // quotaUpdates[0] = - // QuotaUpdate({token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: int96(uint96(quotaLink))}); - // quotaUpdates[1] = - // QuotaUpdate({token: tokenTestSuite.addressOf(Tokens.USDT), quotaChange: int96(uint96(quotaUsdt))}); - - // quotaLink = quotaLink > 1_000_000 * WAD ? uint96(1_000_000 * WAD) : quotaLink; - // quotaUsdt = quotaUsdt > 1_000_000 * WAD ? uint96(1_000_000 * WAD) : quotaUsdt; - - // vm.expectCall( - // address(poolQuotaKeeper), abi.encodeCall(IPoolQuotaKeeper.updateQuotas, (creditAccount, quotaUpdates)) - // ); - - // (uint256 tokensToEnable,) = creditManager.updateQuotas(creditAccount, quotaUpdates); - - // uint256 enabledTokensMap = tokensToEnable | UNDERLYING_TOKEN_MASK; - - // CreditManagerTestInternal(address(creditManager)).setenabledTokensMask(creditAccount, enabledTokensMap); - - // vm.warp(block.timestamp + 60 * 60 * 24 * 365); - - // (,, uint256 totalDebt) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // uint256 expectedTotalDebt = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate; - // expectedTotalDebt += (quotaLink * 1000 + quotaUsdt * 500) / PERCENTAGE_FACTOR; - - // (uint16 feeInterest,,,,) = creditManager.fees(); - - // expectedTotalDebt += ((expectedTotalDebt - borrowedAmount) * feeInterest) / PERCENTAGE_FACTOR; - - // uint256 diff = expectedTotalDebt > totalDebt ? expectedTotalDebt - totalDebt : totalDebt - expectedTotalDebt; - - // assertLe(diff, 2, "Total debt not equal"); - } - - // [DEPRECIATED]: We test that after full collateral - // /// @dev [CMQ-11]: updateQuotas reverts on too many enabled tokens - // function test_CMQ_11_updateQuotas_reverts_on_too_many_tokens_enabled() public { - // (,,, address creditAccount) = cms.openCreditAccount(); - - // uint256 maxTokens = creditManager.maxAllowedEnabledTokenLength(); - - // QuotaUpdate[] memory quotaUpdates = _addManyLimitedTokens(maxTokens + 1, 100); - - // vm.expectRevert(TooManyEnabledTokensException.selector); - // creditManager.updateQuotas(creditAccount, quotaUpdates); - // } - - /// @dev [CMQ-12]: Credit Manager zeroes limits on quoted tokens upon incurring a loss - function test_CMQ_12_creditManager_triggers_limit_zeroing_on_loss() public { - _addQuotedToken(tokenTestSuite.addressOf(Tokens.LINK), 10_00, uint96(1_000_000 * WAD)); - _addQuotedToken(tokenTestSuite.addressOf(Tokens.USDT), 500, uint96(1_000_000 * WAD)); - - (,,, address creditAccount) = cms.openCreditAccount(); - - uint256 enabledTokensMap = UNDERLYING_TOKEN_MASK; - - (uint256 tokensToEnable,) = creditManager.updateQuota({ - creditAccount: creditAccount, - token: tokenTestSuite.addressOf(Tokens.LINK), - quotaChange: int96(uint96(100 * WAD)) - }); - enabledTokensMap |= tokensToEnable; - - (tokensToEnable,) = creditManager.updateQuota({ - creditAccount: creditAccount, - token: tokenTestSuite.addressOf(Tokens.USDT), - quotaChange: int96(uint96(200 * WAD)) - }); - enabledTokensMap |= tokensToEnable; - - address[] memory quotedTokens = new address[](creditManager.maxAllowedEnabledTokenLength() + 1); - - quotedTokens[0] = tokenTestSuite.addressOf(Tokens.LINK); - quotedTokens[1] = tokenTestSuite.addressOf(Tokens.USDT); - - // vm.expectCall(address(poolQuotaKeeper), abi.encodeCall(IPoolQuotaKeeper.setLimitsToZero, (quotedTokens))); - - // creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.LIQUIDATE_ACCOUNT, - // DAI_ACCOUNT_AMOUNT, - // USER, - // USER, - // enabledTokensMap, - // 0, - // DAI_ACCOUNT_AMOUNT, - // false - // ); - - for (uint256 i = 0; i < quotedTokens.length; ++i) { - if (quotedTokens[i] == address(0)) continue; - - (, uint96 limit,,) = poolQuotaKeeper.totalQuotaParams(quotedTokens[i]); - - assertEq(limit, 1, "Limit was not zeroed"); - } - } -} diff --git a/contracts/test/integration/pool/Pool4626.t.sol b/contracts/test/integration/pool/Pool4626.t.sol deleted file mode 100644 index fcadd129..00000000 --- a/contracts/test/integration/pool/Pool4626.t.sol +++ /dev/null @@ -1,1924 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import {IPoolQuotaKeeper} from "../../../interfaces/IPoolQuotaKeeper.sol"; -import {LinearInterestRateModel} from "../../../pool/LinearInterestRateModel.sol"; - -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; - -import {Pool4626} from "../../../pool/Pool4626.sol"; -import {IPool4626Events} from "../../../interfaces/IPool4626.sol"; -import {IERC4626Events} from "../../interfaces/IERC4626.sol"; - -import {IInterestRateModel} from "../../../interfaces/IInterestRateModel.sol"; - -import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; -import {CreditManagerMockForPoolTest} from "../../mocks/pool/CreditManagerMockForPoolTest.sol"; -import { - liquidityProviderInitBalance, - addLiquidity, - removeLiquidity, - referral, - PoolServiceTestSuite -} from "../../suites/PoolServiceTestSuite.sol"; - -import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; -import {Tokens} from "../../config/Tokens.sol"; -import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; -import {ERC20FeeMock} from "../../mocks/token/ERC20FeeMock.sol"; -import {PoolQuotaKeeper} from "../../../pool/PoolQuotaKeeper.sol"; - -// TEST -import {TestHelper} from "../../lib/helper.sol"; -import "forge-std/console.sol"; - -import "../../lib/constants.sol"; - -import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; - -// EXCEPTIONS -import "../../../interfaces/IExceptions.sol"; - -uint256 constant fee = 6000; - -/// @title pool -/// @notice Business logic for borrowing liquidity pools -contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Events { - using Math for uint256; - - PoolServiceTestSuite psts; - PoolQuotaKeeper pqk; - - /* - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. - */ - event Transfer(address indexed from, address indexed to, uint256 value); - - ACL acl; - Pool4626 pool; - address underlying; - CreditManagerMockForPoolTest cmMock; - IInterestRateModel irm; - - function setUp() public { - _setUp(Tokens.DAI, false); - } - - function _setUp(Tokens t, bool supportQuotas) public { - tokenTestSuite = new TokensTestSuite(); - psts = new PoolServiceTestSuite( - tokenTestSuite, - tokenTestSuite.addressOf(t), - true, - supportQuotas - ); - - pool = psts.pool4626(); - irm = psts.linearIRModel(); - underlying = address(psts.underlying()); - cmMock = psts.cmMock(); - acl = psts.acl(); - pqk = psts.poolQuotaKeeper(); - } - - // - // HELPERS - // - function _setUpTestCase( - Tokens t, - uint256 feeToken, - uint16 utilisation, - uint256 availableLiquidity, - uint256 dieselRate, - uint16 withdrawFee, - bool supportQuotas - ) internal { - _setUp(t, supportQuotas); - if (t == Tokens.USDT) { - // set 50% fee if fee token - ERC20FeeMock(pool.asset()).setMaximumFee(type(uint256).max); - ERC20FeeMock(pool.asset()).setBasisPointsRate(feeToken); - } - - _initPoolLiquidity(availableLiquidity, dieselRate); - _connectAndSetLimit(); - - if (utilisation > 0) _borrowToUtilisation(utilisation); - - vm.prank(CONFIGURATOR); - pool.setWithdrawFee(withdrawFee); - } - - function _connectAndSetLimit() internal { - vm.prank(CONFIGURATOR); - pool.setCreditManagerLimit(address(cmMock), type(uint128).max); - } - - function _borrowToUtilisation(uint16 utilisation) internal { - cmMock.lendCreditAccount(pool.expectedLiquidity() / 2, DUMB_ADDRESS); - - assertEq(pool.borrowRate(), irm.calcBorrowRate(PERCENTAGE_FACTOR, utilisation, false)); - } - - function _mulFee(uint256 amount, uint256 _fee) internal pure returns (uint256) { - return (amount * (PERCENTAGE_FACTOR - _fee)) / PERCENTAGE_FACTOR; - } - - function _divFee(uint256 amount, uint256 _fee) internal pure returns (uint256) { - return (amount * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR - _fee); - } - - function _updateBorrowrate() internal { - vm.prank(CONFIGURATOR); - pool.updateInterestRateModel(address(irm)); - } - - function _initPoolLiquidity() internal { - _initPoolLiquidity(addLiquidity, 2 * RAY); - } - - function _initPoolLiquidity(uint256 availableLiquidity, uint256 dieselRate) internal { - assertEq(pool.convertToAssets(RAY), RAY, "Incorrect diesel rate!"); - - vm.prank(INITIAL_LP); - pool.mint(availableLiquidity, INITIAL_LP); - - vm.prank(INITIAL_LP); - pool.burn((availableLiquidity * (dieselRate - RAY)) / dieselRate); - - // assertEq(pool.expectedLiquidityLU(), availableLiquidity * dieselRate / RAY, "ExpectedLU is not correct!"); - assertEq(pool.convertToAssets(RAY), dieselRate, "Incorrect diesel rate!"); - } - - // - // TESTS - // - - // [P4-1]: getDieselRate_RAY=RAY, withdrawFee=0 and expectedLiquidityLimit as expected at start - function test_P4_01_start_parameters_correct() public { - assertEq(pool.name(), "diesel DAI", "Symbol incorrectly set up"); - assertEq(pool.symbol(), "dDAI", "Symbol incorrectly set up"); - assertEq(address(pool.addressProvider()), address(psts.addressProvider()), "Incorrect address provider"); - - assertEq(pool.asset(), underlying, "Incorrect underlying provider"); - assertEq(pool.underlyingToken(), underlying, "Incorrect underlying provider"); - - assertEq(pool.decimals(), IERC20Metadata(address(psts.underlying())).decimals(), "Incorrect decimals"); - - assertEq(pool.treasury(), psts.addressProvider().getTreasuryContract(), "Incorrect treasury"); - - assertEq(pool.convertToAssets(RAY), RAY, "Incorrect diesel rate!"); - - assertEq(address(pool.interestRateModel()), address(psts.linearIRModel()), "Incorrect interest rate model"); - - assertEq(pool.expectedLiquidityLimit(), type(uint256).max); - - assertEq(pool.totalBorrowedLimit(), type(uint256).max); - } - - // [P4-2]: constructor reverts for zero addresses - function test_P4_02_constructor_reverts_for_zero_addresses() public { - vm.expectRevert(ZeroAddressException.selector); - new Pool4626({ - _addressProvider: address(0), - _underlyingToken: underlying, - _interestRateModel: address(psts.linearIRModel()), - _expectedLiquidityLimit: type(uint128).max, - _supportsQuotas: false - }); - - // opts.addressProvider = address(psts.addressProvider()); - // opts.interestRateModel = address(0); - - vm.expectRevert(ZeroAddressException.selector); - new Pool4626({ - _addressProvider:address(psts.addressProvider()), - _underlyingToken: underlying, - _interestRateModel: address(0), - _expectedLiquidityLimit: type(uint128).max, - _supportsQuotas: false - }); - - // opts.interestRateModel = address(psts.linearIRModel()); - // opts.underlyingToken = address(0); - - vm.expectRevert(ZeroAddressException.selector); - new Pool4626({ - _addressProvider: address(psts.addressProvider()), - _underlyingToken: address(0), - _interestRateModel: address(psts.linearIRModel()), - _expectedLiquidityLimit: type(uint128).max, - _supportsQuotas: false - }); - } - - // [P4-3]: constructor emits events - function test_P4_03_constructor_emits_events() public { - uint256 limit = 15890; - - vm.expectEmit(true, false, false, false); - emit SetInterestRateModel(address(psts.linearIRModel())); - - vm.expectEmit(true, false, false, true); - emit SetExpectedLiquidityLimit(limit); - - vm.expectEmit(false, false, false, true); - emit SetTotalBorrowedLimit(limit); - - new Pool4626({ - _addressProvider: address(psts.addressProvider()), - _underlyingToken: underlying, - _interestRateModel: address(psts.linearIRModel()), - _expectedLiquidityLimit: limit, - _supportsQuotas: false - }); - } - - // [P4-4]: addLiquidity, removeLiquidity, lendCreditAccount, repayCreditAccount reverts if contract is paused - function test_P4_04_cannot_be_used_while_paused() public { - vm.startPrank(CONFIGURATOR); - acl.addPausableAdmin(CONFIGURATOR); - pool.pause(); - vm.stopPrank(); - - vm.startPrank(USER); - - vm.expectRevert(bytes(PAUSABLE_ERROR)); - pool.deposit(addLiquidity, FRIEND); - - vm.expectRevert(bytes(PAUSABLE_ERROR)); - pool.depositReferral(addLiquidity, FRIEND, referral); - - vm.expectRevert(bytes(PAUSABLE_ERROR)); - pool.mint(addLiquidity, FRIEND); - - vm.expectRevert(bytes(PAUSABLE_ERROR)); - pool.withdraw(removeLiquidity, FRIEND, FRIEND); - - vm.expectRevert(bytes(PAUSABLE_ERROR)); - pool.redeem(removeLiquidity, FRIEND, FRIEND); - - vm.expectRevert(bytes(PAUSABLE_ERROR)); - pool.lendCreditAccount(1, FRIEND); - - vm.expectRevert(bytes(PAUSABLE_ERROR)); - pool.repayCreditAccount(1, 0, 0); - - vm.stopPrank(); - } - - struct DepositTestCase { - string name; - /// SETUP - Tokens asset; - uint256 tokenFee; - uint256 initialLiquidity; - uint256 dieselRate; - uint16 utilisation; - uint16 withdrawFee; - /// PARAMS - uint256 amountToDeposit; - /// EXPECTED VALUES - uint256 expectedShares; - uint256 expectedAvailableLiquidity; - uint256 expectedLiquidityAfter; - } - - // [P4-5]: deposit adds liquidity correctly - function test_P4_05_deposit_adds_liquidity_correctly() public { - // adds liqudity to mint initial diesel tokens to change 1:1 rate - - DepositTestCase[2] memory cases = [ - DepositTestCase({ - name: "Normal token", - // POOL SETUP - asset: Tokens.DAI, - tokenFee: 0, - initialLiquidity: addLiquidity, - // 1 dDAI = 2 DAI - dieselRate: 2 * RAY, - // 50% of available liquidity is borrowed - utilisation: 50_00, - withdrawFee: 0, - // PARAMS - amountToDeposit: addLiquidity, - // EXPECTED VALUES: - // - // Depends on dieselRate - expectedShares: addLiquidity / 2, - // availableLiquidityBefore: addLiqudity /2 (cause 50% utilisation) - expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity, - expectedLiquidityAfter: addLiquidity * 2 - }), - DepositTestCase({ - name: "Fee token", - /// SETUP - asset: Tokens.USDT, - // transfer fee: 60%, so 40% will be transfer to account - tokenFee: 60_00, - initialLiquidity: addLiquidity, - // 1 dUSDT = 2 USDT - dieselRate: 2 * RAY, - // 50% of available liquidity is borrowed - utilisation: 50_00, - withdrawFee: 0, - /// PARAMS - amountToDeposit: addLiquidity, - /// EXPECTED VALUES - expectedShares: ((addLiquidity * 40) / 100) / 2, - expectedAvailableLiquidity: addLiquidity / 2 + (addLiquidity * 40) / 100, - expectedLiquidityAfter: addLiquidity + (addLiquidity * 40) / 100 - }) - ]; - - for (uint256 i; i < cases.length; ++i) { - DepositTestCase memory testCase = cases[i]; - for (uint256 rc; rc < 2; ++rc) { - bool withReferralCode = rc == 0; - - _setUpTestCase( - testCase.asset, - testCase.tokenFee, - testCase.utilisation, - testCase.initialLiquidity, - testCase.dieselRate, - testCase.withdrawFee, - false - ); - - vm.expectEmit(true, true, false, true); - emit Transfer(address(0), FRIEND, testCase.expectedShares); - - vm.expectEmit(true, true, false, true); - emit Deposit(USER, FRIEND, testCase.amountToDeposit, testCase.expectedShares); - - if (withReferralCode) { - vm.expectEmit(true, true, false, true); - emit DepositWithReferral(USER, FRIEND, testCase.amountToDeposit, referral); - } - - vm.prank(USER); - uint256 shares = withReferralCode - ? pool.depositReferral(testCase.amountToDeposit, FRIEND, referral) - : pool.deposit(testCase.amountToDeposit, FRIEND); - - expectBalance( - address(pool), - FRIEND, - testCase.expectedShares, - _testCaseErr(testCase.name, "Incorrect diesel tokens on FRIEND account") - ); - expectBalance(underlying, USER, liquidityProviderInitBalance - addLiquidity); - assertEq( - pool.expectedLiquidity(), - testCase.expectedLiquidityAfter, - _testCaseErr(testCase.name, "Incorrect expected liquidity") - ); - assertEq( - pool.availableLiquidity(), - testCase.expectedAvailableLiquidity, - _testCaseErr(testCase.name, "Incorrect available liquidity") - ); - assertEq(shares, testCase.expectedShares); - - assertEq( - pool.borrowRate(), - irm.calcBorrowRate(pool.expectedLiquidity(), pool.availableLiquidity(), false), - _testCaseErr(testCase.name, "Borrow rate wasn't update correcty") - ); - } - } - } - - struct MintTestCase { - string name; - /// SETUP - Tokens asset; - uint256 tokenFee; - uint256 initialLiquidity; - uint256 dieselRate; - uint16 utilisation; - uint16 withdrawFee; - /// PARAMS - uint256 desiredShares; - /// EXPECTED VALUES - uint256 expectedAssetsWithdrawal; - uint256 expectedAvailableLiquidity; - uint256 expectedLiquidityAfter; - } - - // [P4-6]: deposit adds liquidity correctly - function test_P4_06_mint_adds_liquidity_correctly() public { - MintTestCase[2] memory cases = [ - MintTestCase({ - name: "Normal token", - // POOL SETUP - asset: Tokens.DAI, - tokenFee: 0, - initialLiquidity: addLiquidity, - // 1 dDAI = 2 DAI - dieselRate: 2 * RAY, - // 50% of available liquidity is borrowed - utilisation: 50_00, - withdrawFee: 0, - // PARAMS - desiredShares: addLiquidity / 2, - // EXPECTED VALUES: - // - // Depends on dieselRate - expectedAssetsWithdrawal: addLiquidity, - // availableLiquidityBefore: addLiqudity /2 (cause 50% utilisation) - expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity, - expectedLiquidityAfter: addLiquidity * 2 - }), - MintTestCase({ - name: "Fee token", - /// SETUP - asset: Tokens.USDT, - // transfer fee: 60%, so 40% will be transfer to account - tokenFee: 60_00, - initialLiquidity: addLiquidity, - // 1 dUSDT = 2 USDT - dieselRate: 2 * RAY, - // 50% of available liquidity is borrowed - utilisation: 50_00, - withdrawFee: 0, - /// PARAMS - desiredShares: addLiquidity / 2, - /// EXPECTED VALUES - /// fee token makes impact on how much tokens will be wiotdrawn from user - expectedAssetsWithdrawal: (addLiquidity * 100) / 40, - expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity, - expectedLiquidityAfter: addLiquidity * 2 - }) - ]; - - for (uint256 i; i < cases.length; ++i) { - MintTestCase memory testCase = cases[i]; - - _setUpTestCase( - testCase.asset, - testCase.tokenFee, - testCase.utilisation, - testCase.initialLiquidity, - testCase.dieselRate, - testCase.withdrawFee, - false - ); - - vm.expectEmit(true, true, false, true); - emit Transfer(address(0), FRIEND, testCase.desiredShares); - - vm.expectEmit(true, true, false, true); - emit Deposit(USER, FRIEND, testCase.expectedAssetsWithdrawal, testCase.desiredShares); - - vm.prank(USER); - uint256 assets = pool.mint(testCase.desiredShares, FRIEND); - - expectBalance( - address(pool), FRIEND, testCase.desiredShares, _testCaseErr(testCase.name, "Incorrect shares ") - ); - expectBalance( - underlying, - USER, - liquidityProviderInitBalance - testCase.expectedAssetsWithdrawal, - _testCaseErr(testCase.name, "Incorrect USER balance") - ); - assertEq( - pool.expectedLiquidity(), - testCase.expectedLiquidityAfter, - _testCaseErr(testCase.name, "Incorrect expected liquidity") - ); - assertEq( - pool.availableLiquidity(), - testCase.expectedAvailableLiquidity, - _testCaseErr(testCase.name, "Incorrect available liquidity") - ); - assertEq( - assets, testCase.expectedAssetsWithdrawal, _testCaseErr(testCase.name, "Incorrect assets return value") - ); - - assertEq( - pool.borrowRate(), - irm.calcBorrowRate(pool.expectedLiquidity(), pool.availableLiquidity(), false), - _testCaseErr(testCase.name, "Borrow rate wasn't update correcty") - ); - } - } - - // [P4-7]: deposit and mint if assets more than limit - function test_P4_07_deposit_and_mint_if_assets_more_than_limit() public { - for (uint256 j; j < 2; ++j) { - for (uint256 i; i < 2; ++i) { - bool feeToken = i == 1; - - Tokens asset = feeToken ? Tokens.USDT : Tokens.DAI; - - _setUpTestCase(asset, feeToken ? 60_00 : 0, 50_00, addLiquidity, 2 * RAY, 0, false); - - vm.prank(CONFIGURATOR); - pool.setExpectedLiquidityLimit(1237882323 * WAD); - - uint256 assetsToReachLimit = pool.expectedLiquidityLimit() - pool.expectedLiquidity(); - - uint256 sharesToReachLimit = assetsToReachLimit / 2; - - if (feeToken) { - assetsToReachLimit = _divFee(assetsToReachLimit, fee); - } - - tokenTestSuite.mint(asset, USER, assetsToReachLimit + 1); - - if (j == 0) { - // DEPOSIT CASE - vm.prank(USER); - pool.deposit(assetsToReachLimit, FRIEND); - } else { - // MINT CASE - vm.prank(USER); - pool.mint(sharesToReachLimit, FRIEND); - } - } - } - } - - // - // WITHDRAW - // - struct WithdrawTestCase { - string name; - /// SETUP - Tokens asset; - uint256 tokenFee; - uint256 initialLiquidity; - uint256 dieselRate; - uint16 utilisation; - uint16 withdrawFee; - /// PARAMS - uint256 sharesToMint; - uint256 assetsToWithdraw; - /// EXPECTED VALUES - uint256 expectedSharesBurnt; - uint256 expectedAvailableLiquidity; - uint256 expectedLiquidityAfter; - uint256 expectedTreasury; - } - - // [P4-8]: deposit and mint if assets more than limit - function test_P4_08_withdraw_works_as_expected() public { - WithdrawTestCase[4] memory cases = [ - WithdrawTestCase({ - name: "Normal token with 0 withdraw fee", - // POOL SETUP - asset: Tokens.DAI, - tokenFee: 0, - initialLiquidity: addLiquidity, - // 1 dDAI = 2 DAI - dieselRate: 2 * RAY, - // 50% of available liquidity is borrowed - utilisation: 50_00, - withdrawFee: 0, - // PARAMS - sharesToMint: addLiquidity / 2, - assetsToWithdraw: addLiquidity / 4, - // EXPECTED VALUES: - // - // Depends on dieselRate - expectedSharesBurnt: addLiquidity / 8, - // availableLiquidityBefore: addLiqudity /2 (cause 50% utilisation) - expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 4, - expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 4, - expectedTreasury: 0 - }), - WithdrawTestCase({ - name: "Normal token with 1% withdraw fee", - // POOL SETUP - asset: Tokens.DAI, - tokenFee: 0, - initialLiquidity: addLiquidity, - // 1 dDAI = 2 DAI - dieselRate: 2 * RAY, - // 50% of available liquidity is borrowed - utilisation: 50_00, - withdrawFee: 1_00, - // PARAMS - sharesToMint: addLiquidity / 2, - assetsToWithdraw: addLiquidity / 4, - // EXPECTED VALUES: - // - // Depends on dieselRate - expectedSharesBurnt: ((addLiquidity / 8) * 100) / 99, - expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - ((addLiquidity / 4) * 100) / 99, - expectedLiquidityAfter: addLiquidity * 2 - ((addLiquidity / 4) * 100) / 99, - expectedTreasury: ((addLiquidity / 4) * 1) / 99 - }), - WithdrawTestCase({ - name: "Fee token with 0 withdraw fee", - /// SETUP - asset: Tokens.USDT, - // transfer fee: 60%, so 40% will be transfer to account - tokenFee: 60_00, - initialLiquidity: addLiquidity, - // 1 dUSDT = 2 USDT - dieselRate: 2 * RAY, - // 50% of available liquidity is borrowed - utilisation: 50_00, - withdrawFee: 0, - // PARAMS - sharesToMint: addLiquidity / 2, - assetsToWithdraw: addLiquidity / 4, - // EXPECTED VALUES: - // - // Depends on dieselRate - expectedSharesBurnt: ((addLiquidity / 8) * 100) / 40, - expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - ((addLiquidity / 4) * 100) / 40, - expectedLiquidityAfter: addLiquidity * 2 - ((addLiquidity / 4) * 100) / 40, - expectedTreasury: 0 - }), - WithdrawTestCase({ - name: "Fee token with 1% withdraw fee", - /// SETUP - asset: Tokens.USDT, - // transfer fee: 60%, so 40% will be transfer to account - tokenFee: 60_00, - initialLiquidity: addLiquidity, - // 1 dUSDT = 2 USDT - dieselRate: 2 * RAY, - // 50% of available liquidity is borrowed - utilisation: 50_00, - withdrawFee: 1_00, - // PARAMS - sharesToMint: addLiquidity / 2, - assetsToWithdraw: addLiquidity / 4, - // EXPECTED VALUES: - // - // addLiquidity /2 * 1/2 (rate) * 1 / (100%-1%) / feeToken - expectedSharesBurnt: ((((addLiquidity / 8) * 100) / 99) * 100) / 40 + 1, - // availableLiquidityBefore: addLiqudity /2 (cause 50% utilisation) - expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - ((((addLiquidity / 4) * 100) / 40) * 100) / 99 - 1, - expectedLiquidityAfter: addLiquidity * 2 - ((((addLiquidity / 4) * 100) / 40) * 100) / 99 - 1, - expectedTreasury: ((addLiquidity / 4) * 1) / 99 + 1 - }) - ]; - - for (uint256 i; i < cases.length; ++i) { - WithdrawTestCase memory testCase = cases[i]; - /// @dev a represents allowance, 0 means required amount +1, 1 means inlimited allowance - for (uint256 approveCase; approveCase < 2; ++approveCase) { - _setUpTestCase( - testCase.asset, - testCase.tokenFee, - testCase.utilisation, - testCase.initialLiquidity, - testCase.dieselRate, - testCase.withdrawFee, - false - ); - - vm.prank(USER); - pool.mint(testCase.sharesToMint, FRIEND); - - vm.prank(FRIEND); - pool.approve(USER, approveCase == 0 ? testCase.expectedSharesBurnt + 1 : type(uint256).max); - - vm.expectEmit(true, true, false, true); - emit Transfer(FRIEND, address(0), testCase.expectedSharesBurnt); - - vm.expectEmit(true, true, false, true); - emit Withdraw(USER, FRIEND2, FRIEND, testCase.assetsToWithdraw, testCase.expectedSharesBurnt); - - vm.prank(USER); - uint256 shares = pool.withdraw(testCase.assetsToWithdraw, FRIEND2, FRIEND); - - expectBalance( - underlying, - FRIEND2, - testCase.assetsToWithdraw, - _testCaseErr(testCase.name, "Incorrect assets on FRIEND2 account") - ); - - expectBalance( - underlying, - pool.treasury(), - testCase.expectedTreasury, - _testCaseErr(testCase.name, "Incorrect DAO fee") - ); - assertEq( - shares, testCase.expectedSharesBurnt, _testCaseErr(testCase.name, "Incorrect shares return value") - ); - - expectBalance( - address(pool), - FRIEND, - testCase.sharesToMint - testCase.expectedSharesBurnt, - _testCaseErr(testCase.name, "Incorrect FRIEND balance") - ); - - assertEq( - pool.expectedLiquidity(), - testCase.expectedLiquidityAfter, - _testCaseErr(testCase.name, "Incorrect expected liquidity") - ); - assertEq( - pool.availableLiquidity(), - testCase.expectedAvailableLiquidity, - _testCaseErr(testCase.name, "Incorrect available liquidity") - ); - - assertEq( - pool.allowance(FRIEND, USER), - approveCase == 0 ? 1 : type(uint256).max, - _testCaseErr(testCase.name, "Incorrect allowance after operation") - ); - - assertEq( - pool.borrowRate(), - irm.calcBorrowRate(pool.expectedLiquidity(), pool.availableLiquidity(), false), - _testCaseErr(testCase.name, "Borrow rate wasn't update correcty") - ); - } - } - } - - // - // REDEEM - // - struct RedeemTestCase { - string name; - /// SETUP - Tokens asset; - uint256 tokenFee; - uint256 initialLiquidity; - uint256 dieselRate; - uint16 utilisation; - uint16 withdrawFee; - /// PARAMS - uint256 sharesToMint; - uint256 sharesToRedeem; - /// EXPECTED VALUES - uint256 expectedAssetsDelivered; - uint256 expectedAvailableLiquidity; - uint256 expectedLiquidityAfter; - uint256 expectedTreasury; - } - - // [P4-9]: deposit and mint if assets more than limit - function test_P4_09_redeem_works_as_expected() public { - RedeemTestCase[4] memory cases = [ - RedeemTestCase({ - name: "Normal token with 0 withdraw fee", - // POOL SETUP - asset: Tokens.DAI, - tokenFee: 0, - initialLiquidity: addLiquidity, - // 1 dDAI = 2 DAI - dieselRate: 2 * RAY, - // 50% of available liquidity is borrowed - utilisation: 50_00, - withdrawFee: 0, - // PARAMS - sharesToMint: addLiquidity / 2, - sharesToRedeem: addLiquidity / 4, - // EXPECTED VALUES: - // - // Depends on dieselRate - expectedAssetsDelivered: addLiquidity / 2, - // availableLiquidityBefore: addLiqudity /2 (cause 50% utilisation) - expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 2, - expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 2, - expectedTreasury: 0 - }), - RedeemTestCase({ - name: "Normal token with 1% withdraw fee", - // POOL SETUP - asset: Tokens.DAI, - tokenFee: 0, - initialLiquidity: addLiquidity, - // 1 dDAI = 2 DAI - dieselRate: 2 * RAY, - // 50% of available liquidity is borrowed - utilisation: 50_00, - withdrawFee: 1_00, - // PARAMS - sharesToMint: addLiquidity / 2, - sharesToRedeem: addLiquidity / 4, - // EXPECTED VALUES: - // - // Depends on dieselRate - expectedAssetsDelivered: ((addLiquidity / 2) * 99) / 100, - expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 2, - expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 2, - expectedTreasury: ((addLiquidity / 2) * 1) / 100 - }), - RedeemTestCase({ - name: "Fee token with 0 withdraw fee", - /// SETUP - asset: Tokens.USDT, - // transfer fee: 60%, so 40% will be transfer to account - tokenFee: 60_00, - initialLiquidity: addLiquidity, - // 1 dUSDT = 2 USDT - dieselRate: 2 * RAY, - // 50% of available liquidity is borrowed - utilisation: 50_00, - withdrawFee: 0, - // PARAMS - sharesToMint: addLiquidity / 2, - sharesToRedeem: addLiquidity / 4, - // EXPECTED VALUES: - // - // Depends on dieselRate - expectedAssetsDelivered: ((addLiquidity / 2) * 40) / 100, - expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 2, - expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 2, - expectedTreasury: 0 - }), - RedeemTestCase({ - name: "Fee token with 1% withdraw fee", - /// SETUP - asset: Tokens.USDT, - // transfer fee: 60%, so 40% will be transfer to account - tokenFee: 60_00, - initialLiquidity: addLiquidity, - // 1 dUSDT = 2 USDT - dieselRate: 2 * RAY, - // 50% of available liquidity is borrowed - utilisation: 50_00, - withdrawFee: 1_00, - // PARAMS - sharesToMint: addLiquidity / 2, - sharesToRedeem: addLiquidity / 4, - // EXPECTED VALUES: - // - // addLiquidity /2 * 1/2 (rate) * 1 / (100%-1%) / feeToken - expectedAssetsDelivered: ((((addLiquidity / 2) * 99) / 100) * 40) / 100, - // availableLiquidityBefore: addLiqudity /2 (cause 50% utilisation) - expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 2, - expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 2, - expectedTreasury: ((((addLiquidity / 2) * 40) / 100) * 1) / 100 - }) - ]; - /// @dev a represents allowance, 0 means required amount +1, 1 means inlimited allowance - - for (uint256 i; i < cases.length; ++i) { - RedeemTestCase memory testCase = cases[i]; - for (uint256 approveCase; approveCase < 2; ++approveCase) { - _setUpTestCase( - testCase.asset, - testCase.tokenFee, - testCase.utilisation, - testCase.initialLiquidity, - testCase.dieselRate, - testCase.withdrawFee, - false - ); - - vm.prank(USER); - pool.mint(testCase.sharesToMint, FRIEND); - - vm.prank(FRIEND); - pool.approve(USER, approveCase == 0 ? testCase.sharesToRedeem + 1 : type(uint256).max); - - vm.expectEmit(true, true, false, true); - emit Transfer(FRIEND, address(0), testCase.sharesToRedeem); - - vm.expectEmit(true, true, false, true); - emit Withdraw(USER, FRIEND2, FRIEND, testCase.expectedAssetsDelivered, testCase.sharesToRedeem); - - vm.prank(USER); - uint256 assets = pool.redeem(testCase.sharesToRedeem, FRIEND2, FRIEND); - - expectBalance( - underlying, - FRIEND2, - testCase.expectedAssetsDelivered, - _testCaseErr(testCase.name, "Incorrect assets on FRIEND2 account ") - ); - - expectBalance( - underlying, - pool.treasury(), - testCase.expectedTreasury, - _testCaseErr(testCase.name, "Incorrect treasury fee") - ); - assertEq( - assets, - testCase.expectedAssetsDelivered, - _testCaseErr(testCase.name, "Incorrect assets return value") - ); - expectBalance( - address(pool), - FRIEND, - testCase.sharesToMint - testCase.sharesToRedeem, - _testCaseErr(testCase.name, "Incorrect FRIEND balance") - ); - - assertEq( - pool.expectedLiquidity(), - testCase.expectedLiquidityAfter, - _testCaseErr(testCase.name, "Incorrect expected liquidity") - ); - assertEq( - pool.availableLiquidity(), - testCase.expectedAvailableLiquidity, - _testCaseErr(testCase.name, "Incorrect available liquidity") - ); - - assertEq( - pool.allowance(FRIEND, USER), - approveCase == 0 ? 1 : type(uint256).max, - _testCaseErr(testCase.name, "Incorrect allowance after operation") - ); - - assertEq( - pool.borrowRate(), - irm.calcBorrowRate(pool.expectedLiquidity(), pool.availableLiquidity(), false), - _testCaseErr(testCase.name, "Borrow rate wasn't update correcty") - ); - } - } - } - - // [P4-10]: burn works as expected - function test_P4_10_burn_works_as_expected() public { - _setUpTestCase(Tokens.DAI, 0, 50_00, addLiquidity, 2 * RAY, 0, false); - - vm.prank(USER); - pool.mint(addLiquidity, USER); - - expectBalance(address(pool), USER, addLiquidity, "SETUP: Incorrect USER balance"); - - /// Initial lp provided 1/2 AL + 1AL from USER - assertEq(pool.totalSupply(), (addLiquidity * 3) / 2, "SETUP: Incorrect total supply"); - - uint256 borrowRate = pool.borrowRate(); - uint256 dieselRate = pool.convertToAssets(RAY); - uint256 availableLiquidity = pool.availableLiquidity(); - uint256 expectedLiquidity = pool.expectedLiquidity(); - - vm.prank(USER); - pool.burn(addLiquidity / 4); - - expectBalance(address(pool), USER, (addLiquidity * 3) / 4, "Incorrect USER balance"); - - assertEq(pool.borrowRate(), borrowRate, "Incorrect borrow rate"); - /// Before burn totalSupply was 150% * AL, after 125% * LP - assertEq(pool.convertToAssets(RAY), (dieselRate * 150) / 125, "Incorrect diesel rate"); - assertEq(pool.availableLiquidity(), availableLiquidity, "Incorrect available liquidity"); - assertEq(pool.expectedLiquidity(), expectedLiquidity, "Incorrect expected liquidity"); - } - - /// - /// LEND CREDIT ACCOUNT - // [P4-11]: lendCreditAccount works as expected - function test_P4_11_lendCreditAccount_works_as_expected() public { - _setUpTestCase(Tokens.DAI, 0, 0, addLiquidity, 2 * RAY, 0, false); - - address creditAccount = DUMB_ADDRESS; - uint256 borrowAmount = addLiquidity / 5; - - expectBalance(pool.asset(), creditAccount, 0, "SETUP: incorrect CA balance"); - assertEq(pool.borrowRate(), irm.R_base_RAY(), "SETUP: incorrect borrowRate"); - assertEq(pool.totalBorrowed(), 0, "SETUP: incorrect totalBorrowed"); - assertEq(pool.creditManagerBorrowed(address(cmMock)), 0, "SETUP: incorrect CM limit"); - - uint256 availableLiquidityBefore = pool.availableLiquidity(); - uint256 expectedLiquidityBefore = pool.expectedLiquidity(); - - vm.expectEmit(true, true, false, true); - emit Transfer(address(pool), creditAccount, borrowAmount); - - vm.expectEmit(true, true, false, true); - emit Borrow(address(cmMock), creditAccount, borrowAmount); - - cmMock.lendCreditAccount(borrowAmount, creditAccount); - - assertEq(pool.availableLiquidity(), availableLiquidityBefore - borrowAmount, "Incorrect available liquidity"); - assertEq(pool.expectedLiquidity(), expectedLiquidityBefore, "Incorrect expected liquidity"); - assertEq(pool.totalBorrowed(), borrowAmount, "Incorrect borrowAmount"); - - assertEq( - pool.borrowRate(), - irm.calcBorrowRate(pool.expectedLiquidity(), pool.availableLiquidity(), false), - "Borrow rate wasn't update correcty" - ); - - assertEq(pool.creditManagerBorrowed(address(cmMock)), borrowAmount, "Incorrect CM limit"); - } - - // [P4-12]: lendCreditAccount reverts if it breaches limits - function test_P4_12_lendCreditAccount_reverts_if_breach_limits() public { - address creditAccount = DUMB_ADDRESS; - - _setUpTestCase(Tokens.DAI, 0, 0, addLiquidity, 2 * RAY, 0, false); - - vm.expectRevert(CreditManagerCantBorrowException.selector); - cmMock.lendCreditAccount(0, creditAccount); - - vm.startPrank(CONFIGURATOR); - pool.setCreditManagerLimit(address(cmMock), type(uint128).max); - pool.setTotalBorrowedLimit(addLiquidity); - vm.stopPrank(); - - vm.expectRevert(CreditManagerCantBorrowException.selector); - cmMock.lendCreditAccount(addLiquidity + 1, creditAccount); - - vm.startPrank(CONFIGURATOR); - pool.setCreditManagerLimit(address(cmMock), addLiquidity); - pool.setTotalBorrowedLimit(type(uint128).max); - vm.stopPrank(); - - vm.expectRevert(CreditManagerCantBorrowException.selector); - cmMock.lendCreditAccount(addLiquidity + 1, creditAccount); - } - - // - // REPAY - // - - // [P4-13]: repayCreditAccount reverts for incorrect credit managers - function test_P4_13_repayCreditAccount_reverts_for_incorrect_credit_managers() public { - _setUpTestCase(Tokens.DAI, 0, 0, addLiquidity, 2 * RAY, 0, false); - - /// Case for unknown CM - vm.expectRevert(CallerNotCreditManagerException.selector); - vm.prank(USER); - pool.repayCreditAccount(1, 0, 0); - - /// Case for CM with zero debt - assertEq(pool.creditManagerBorrowed(address(cmMock)), 0, "SETUP: Incorrect CM limit"); - - vm.expectRevert(CallerNotCreditManagerException.selector); - cmMock.repayCreditAccount(1, 0, 0); - } - - struct RepayTestCase { - string name; - /// SETUP - Tokens asset; - uint256 tokenFee; - uint256 initialLiquidity; - uint256 dieselRate; - uint256 sharesInTreasury; - uint256 borrowBefore; - /// PARAMS - uint256 borrowAmount; - uint256 profit; - uint256 loss; - /// EXPECTED VALUES - uint256 expectedTotalSupply; - uint256 expectedAvailableLiquidity; - uint256 expectedLiquidityAfter; - uint256 expectedTreasury; - uint256 uncoveredLoss; - } - - // [P4-14]: repayCreditAccount works as expected - function test_P4_14_repayCreditAccount_works_as_expected() public { - address creditAccount = DUMB_ADDRESS; - RepayTestCase[5] memory cases = [ - RepayTestCase({ - name: "profit: 0, loss: 0", - // POOL SETUP - asset: Tokens.DAI, - tokenFee: 0, - initialLiquidity: 2 * addLiquidity, - // 1 dDAI = 2 DAI - dieselRate: 2 * RAY, - // No borrowing on start - borrowBefore: addLiquidity, - sharesInTreasury: addLiquidity / 4, - // PARAMS - borrowAmount: addLiquidity / 2, - profit: 0, - loss: 0, - // EXPECTED VALUES: - // - // Depends on dieselRate - expectedTotalSupply: addLiquidity, - expectedAvailableLiquidity: 2 * addLiquidity - addLiquidity + addLiquidity / 2, - expectedLiquidityAfter: 2 * addLiquidity, - expectedTreasury: 0, - uncoveredLoss: 0 - }), - RepayTestCase({ - name: "profit: 10%, loss: 0", - // POOL SETUP - asset: Tokens.DAI, - tokenFee: 0, - initialLiquidity: 2 * addLiquidity, - // 1 dDAI = 2 DAI - dieselRate: 2 * RAY, - // No borrowing on start - borrowBefore: addLiquidity, - sharesInTreasury: addLiquidity / 4, - // PARAMS - borrowAmount: addLiquidity / 2, - profit: (addLiquidity * 1) / 10, - loss: 0, - // EXPECTED VALUES: - // - // addLiqudity + new minted diesel tokens for 10% with rate 2:1 - expectedTotalSupply: addLiquidity + (addLiquidity * 1) / 10 / 2, - expectedAvailableLiquidity: 2 * addLiquidity - addLiquidity + addLiquidity / 2 + (addLiquidity * 1) / 10, - // added profit here - expectedLiquidityAfter: 2 * addLiquidity + (addLiquidity * 1) / 10, - expectedTreasury: 0, - uncoveredLoss: 0 - }), - RepayTestCase({ - name: "profit: 0, loss: 10% (covered)", - // POOL SETUP - asset: Tokens.DAI, - tokenFee: 0, - initialLiquidity: 2 * addLiquidity, - // 1 dDAI = 2 DAI - dieselRate: 2 * RAY, - // No borrowing on start - borrowBefore: addLiquidity, - sharesInTreasury: addLiquidity / 4, - // PARAMS - borrowAmount: addLiquidity / 2, - profit: 0, - loss: (addLiquidity * 1) / 10, - // EXPECTED VALUES: - // - // with covered loss, the system should burn DAO shares based on current rate - expectedTotalSupply: addLiquidity - (addLiquidity * 1) / 10 / 2, - expectedAvailableLiquidity: 2 * addLiquidity - addLiquidity + addLiquidity / 2 - (addLiquidity * 1) / 10, - expectedLiquidityAfter: 2 * addLiquidity - (addLiquidity * 1) / 10, - expectedTreasury: 0, - uncoveredLoss: 0 - }), - RepayTestCase({ - name: "profit: 0, loss: 10% (uncovered)", - // POOL SETUP - asset: Tokens.DAI, - tokenFee: 0, - initialLiquidity: 2 * addLiquidity, - // 1 dDAI = 2 DAI - dieselRate: 2 * RAY, - // No borrowing on start - borrowBefore: addLiquidity, - sharesInTreasury: 0, - // PARAMS - borrowAmount: addLiquidity / 2, - profit: 0, - loss: (addLiquidity * 1) / 10, - // EXPECTED VALUES: - // - // Depends on dieselRate - expectedTotalSupply: addLiquidity, - expectedAvailableLiquidity: 2 * addLiquidity - addLiquidity + addLiquidity / 2 - (addLiquidity * 1) / 10, - expectedLiquidityAfter: 2 * addLiquidity - (addLiquidity * 1) / 10, - expectedTreasury: 0, - uncoveredLoss: (addLiquidity * 1) / 10 - }), - RepayTestCase({ - name: "profit: 0, loss: 20% (partially covered)", - // POOL SETUP - asset: Tokens.DAI, - tokenFee: 0, - initialLiquidity: 2 * addLiquidity, - // 1 dDAI = 2 DAI - dieselRate: 2 * RAY, - // No borrowing on start - borrowBefore: addLiquidity, - sharesInTreasury: (addLiquidity * 1) / 10 / 2, - // PARAMS - borrowAmount: addLiquidity / 2, - profit: 0, - loss: (addLiquidity * 2) / 10, - // EXPECTED VALUES: - // - // Depends on dieselRate - expectedTotalSupply: addLiquidity - (addLiquidity * 1) / 10 / 2, - expectedAvailableLiquidity: 2 * addLiquidity - addLiquidity + addLiquidity / 2 - (addLiquidity * 2) / 10, - expectedLiquidityAfter: 2 * addLiquidity - (addLiquidity * 2) / 10, - expectedTreasury: 0, - uncoveredLoss: (addLiquidity * 1) / 10 - }) - ]; - for (uint256 i; i < cases.length; ++i) { - RepayTestCase memory testCase = cases[i]; - - _setUpTestCase( - testCase.asset, - testCase.tokenFee, - // sets utilisation to 0 - 0, - testCase.initialLiquidity, - testCase.dieselRate, - // sets withdrawFee to 0 - 0, - false - ); - - address treasury = pool.treasury(); - - vm.prank(INITIAL_LP); - pool.transfer(treasury, testCase.sharesInTreasury); - - cmMock.lendCreditAccount(testCase.borrowBefore, creditAccount); - - assertEq(pool.totalBorrowed(), testCase.borrowBefore, "SETUP: incorrect totalBorrowed"); - assertEq(pool.creditManagerBorrowed(address(cmMock)), testCase.borrowBefore, "SETUP: Incorrect CM limit"); - - vm.startPrank(creditAccount); - IERC20(pool.asset()).transfer(address(pool), testCase.borrowAmount + testCase.profit - testCase.loss); - vm.stopPrank(); - - if (testCase.uncoveredLoss > 0) { - vm.expectEmit(true, false, false, true); - emit ReceiveUncoveredLoss(address(cmMock), testCase.uncoveredLoss); - } - - vm.expectEmit(true, true, false, true); - emit Repay(address(cmMock), testCase.borrowAmount, testCase.profit, testCase.loss); - - uint256 dieselRate = pool.convertToAssets(RAY); - - cmMock.repayCreditAccount(testCase.borrowAmount, testCase.profit, testCase.loss); - - if (testCase.uncoveredLoss == 0) { - assertEq(dieselRate, pool.convertToAssets(RAY), "Unexpceted change in borrow rate"); - } - - assertEq( - pool.totalSupply(), testCase.expectedTotalSupply, _testCaseErr(testCase.name, "Incorrect total supply") - ); - - assertEq( - pool.totalBorrowed(), - testCase.borrowBefore - testCase.borrowAmount, - _testCaseErr(testCase.name, "incorrect totalBorrowed") - ); - - assertEq( - pool.creditManagerBorrowed(address(cmMock)), - testCase.borrowBefore - testCase.borrowAmount, - "SETUP: Incorrect CM limit" - ); - - expectBalance( - underlying, - pool.treasury(), - testCase.expectedTreasury, - _testCaseErr(testCase.name, "Incorrect treasury fee") - ); - - assertEq( - pool.expectedLiquidity(), - testCase.expectedLiquidityAfter, - _testCaseErr(testCase.name, "Incorrect expected liquidity") - ); - assertEq( - pool.availableLiquidity(), - testCase.expectedAvailableLiquidity, - _testCaseErr(testCase.name, "Incorrect available liquidity") - ); - } - } - - /// - /// CALC LINEAR CUMULATIVE - /// - - // [P4-15]: calcLinearCumulative_RAY computes correctly - function test_P4_15_calcLinearCumulative_RAY_correct() public { - _setUpTestCase(Tokens.DAI, 0, 50_00, addLiquidity, 2 * RAY, 0, false); - - uint256 timeWarp = 180 days; - - vm.warp(block.timestamp + timeWarp); - - uint256 borrowRate = pool.borrowRate(); - - uint256 expectedLinearRate = RAY + (borrowRate * timeWarp) / 365 days; - - assertEq(pool.calcLinearCumulative_RAY(), expectedLinearRate, "Index value was not updated correctly"); - } - - // [P4-16]: updateBorrowRate correctly updates parameters - function test_P4_16_updateBorrowRate_correct() public { - uint256 quotaInterestPerYear = addLiquidity / 4; - for (uint256 i; i < 2; ++i) { - bool supportQuotas = i == 1; - string memory testName = supportQuotas ? "Test with supportQuotas=true" : "Test with supportQuotas=false"; - - _setUpTestCase(Tokens.DAI, 0, 50_00, addLiquidity, 2 * RAY, 0, supportQuotas); - - if (supportQuotas) { - vm.startPrank(CONFIGURATOR); - psts.gaugeMock().addQuotaToken(tokenTestSuite.addressOf(Tokens.LINK), 100_00); - - pqk.addCreditManager(address(cmMock)); - - cmMock.addToken(tokenTestSuite.addressOf(Tokens.LINK), 2); - - pqk.setTokenLimit(tokenTestSuite.addressOf(Tokens.LINK), uint96(WAD * 100_000)); - - cmMock.updateQuota({ - _creditAccount: DUMB_ADDRESS, - token: tokenTestSuite.addressOf(Tokens.LINK), - quotaChange: int96(int256(quotaInterestPerYear)) - }); - - psts.gaugeMock().updateEpoch(); - - vm.stopPrank(); - } - - uint256 borrowRate = pool.borrowRate(); - uint256 timeWarp = 365 days; - - vm.warp(block.timestamp + timeWarp); - - uint256 expectedInterest = ((addLiquidity / 2) * borrowRate) / RAY; - uint256 expectedLiquidity = addLiquidity + expectedInterest + (supportQuotas ? quotaInterestPerYear : 0); - - uint256 expectedBorrowRate = psts.linearIRModel().calcBorrowRate(expectedLiquidity, addLiquidity / 2); - - _updateBorrowrate(); - - assertEq( - pool.expectedLiquidity(), - expectedLiquidity, - _testCaseErr(testName, "Expected liquidity was not updated correctly") - ); - - // should not take quota interest - - assertEq( - pool.expectedLiquidityLU(), - addLiquidity + expectedInterest, - _testCaseErr(testName, "ExpectedLU liquidity was not updated correctly") - ); - - assertEq( - uint256(pool.timestampLU()), - block.timestamp, - _testCaseErr(testName, "Timestamp was not updated correctly") - ); - - assertEq( - pool.borrowRate(), expectedBorrowRate, _testCaseErr(testName, "Borrow rate was not updated correctly") - ); - - assertEq( - pool.calcLinearCumulative_RAY(), - pool.cumulativeIndexLU_RAY(), - _testCaseErr(testName, "Index value was not updated correctly") - ); - } - } - - // [P4-17]: updateBorrowRate correctly updates parameters - function test_P4_17_changeQuotaRevenue_and_updateQuotaRevenue_updates_quotaRevenue_correctly() public { - _setUp(Tokens.DAI, true); - address POOL_QUOTA_KEEPER = address(pqk); - - uint96 qu1 = uint96(WAD * 10); - - assertEq(pool.lastQuotaRevenueUpdate(), 0, "SETUP: Incorrect lastQuotaRevenuUpdate"); - - assertEq(pool.quotaRevenue(), 0, "SETUP: Incorrect quotaRevenue"); - assertEq(pool.expectedLiquidityLU(), 0, "SETUP: Incorrect expectedLiquidityLU"); - - vm.prank(POOL_QUOTA_KEEPER); - pool.updateQuotaRevenue(qu1); - - assertEq(pool.lastQuotaRevenueUpdate(), block.timestamp, "#1: Incorrect lastQuotaRevenuUpdate"); - assertEq(pool.quotaRevenue(), qu1, "#1: Incorrect quotaRevenue"); - - assertEq(pool.expectedLiquidityLU(), 0, "#1: Incorrect expectedLiquidityLU"); - - uint256 year = 365 days; - - vm.warp(block.timestamp + year); - - uint96 qu2 = uint96(WAD * 15); - - vm.prank(POOL_QUOTA_KEEPER); - pool.updateQuotaRevenue(qu2); - - assertEq(pool.lastQuotaRevenueUpdate(), block.timestamp, "#2: Incorrect lastQuotaRevenuUpdate"); - assertEq(pool.quotaRevenue(), qu2, "#2: Incorrect quotaRevenue"); - - assertEq(pool.expectedLiquidityLU(), qu1 / PERCENTAGE_FACTOR, "#2: Incorrect expectedLiquidityLU"); - - vm.warp(block.timestamp + year); - - uint96 dqu = uint96(WAD * 5); - - vm.prank(POOL_QUOTA_KEEPER); - pool.changeQuotaRevenue(-int96(dqu)); - - assertEq(pool.lastQuotaRevenueUpdate(), block.timestamp, "#3: Incorrect lastQuotaRevenuUpdate"); - assertEq(pool.quotaRevenue(), qu2 - dqu, "#3: Incorrect quotaRevenue"); - - assertEq(pool.expectedLiquidityLU(), (qu1 + qu2) / PERCENTAGE_FACTOR, "#3: Incorrect expectedLiquidityLU"); - } - - // [P4-18]: connectCreditManager, forbidCreditManagerToBorrow, newInterestRateModel, setExpecetedLiquidityLimit reverts if called with non-configurator - function test_P4_18_admin_functions_revert_on_non_admin() public { - vm.startPrank(USER); - - vm.expectRevert(CallerNotControllerException.selector); - pool.setCreditManagerLimit(DUMB_ADDRESS, 1); - - vm.expectRevert(CallerNotConfiguratorException.selector); - pool.updateInterestRateModel(DUMB_ADDRESS); - - vm.expectRevert(CallerNotConfiguratorException.selector); - pool.connectPoolQuotaManager(DUMB_ADDRESS); - - vm.expectRevert(CallerNotControllerException.selector); - pool.setExpectedLiquidityLimit(0); - - vm.expectRevert(CallerNotControllerException.selector); - pool.setTotalBorrowedLimit(0); - - vm.expectRevert(CallerNotControllerException.selector); - pool.setWithdrawFee(0); - - vm.stopPrank(); - } - - // [P4-19]: setCreditManagerLimit reverts if not in register - function test_P4_19_connectCreditManager_reverts_if_not_in_register() public { - vm.expectRevert(RegisteredCreditManagerOnlyException.selector); - - vm.prank(CONFIGURATOR); - pool.setCreditManagerLimit(DUMB_ADDRESS, 1); - } - - // [P4-20]: setCreditManagerLimit reverts if another pool is setup in CreditManagerV3 - function test_P4_20_connectCreditManager_fails_on_incompatible_CM() public { - cmMock.changePoolService(DUMB_ADDRESS); - - vm.expectRevert(IncompatibleCreditManagerException.selector); - - vm.prank(CONFIGURATOR); - pool.setCreditManagerLimit(address(cmMock), 1); - } - - // [P4-21]: setCreditManagerLimit connects manager first time, then update limit only - function test_P4_21_setCreditManagerLimit_connects_manager_first_time_then_update_limit_only() public { - address[] memory cms = pool.creditManagers(); - assertEq(cms.length, 0, "Credit manager is already connected!"); - - vm.expectEmit(true, true, false, false); - emit AddCreditManager(address(cmMock)); - - vm.expectEmit(true, true, false, true); - emit BorrowLimitChanged(address(cmMock), 230); - - vm.prank(CONFIGURATOR); - pool.setCreditManagerLimit(address(cmMock), 230); - - cms = pool.creditManagers(); - assertEq(cms.length, 1, "#1: Credit manager is already connected!"); - assertEq(cms[0], address(cmMock), "#1: Credit manager is not connected!"); - - assertEq(pool.creditManagerLimit(address(cmMock)), 230, "#1: Incorrect CM limit"); - - vm.expectEmit(true, true, false, true); - emit BorrowLimitChanged(address(cmMock), 150); - - vm.prank(CONFIGURATOR); - pool.setCreditManagerLimit(address(cmMock), 150); - - cms = pool.creditManagers(); - assertEq(cms.length, 1, "#2: Credit manager is already connected!"); - assertEq(cms[0], address(cmMock), "#2: Credit manager is not connected!"); - assertEq(pool.creditManagerLimit(address(cmMock)), 150, "#2: Incorrect CM limit"); - - vm.prank(CONFIGURATOR); - pool.setCreditManagerLimit(address(cmMock), type(uint256).max); - - assertEq(pool.creditManagerLimit(address(cmMock)), type(uint256).max, "#3: Incorrect CM limit"); - } - - // [P4-22]: updateInterestRateModel changes interest rate model & emit event - function test_P4_22_updateInterestRateModel_works_correctly_and_emits_event() public { - _setUpTestCase(Tokens.DAI, 0, 50_00, addLiquidity, 2 * RAY, 0, false); - - uint256 expectedLiquidity = pool.expectedLiquidity(); - uint256 availableLiquidity = pool.availableLiquidity(); - - LinearInterestRateModel newIR = new LinearInterestRateModel( - 8000, - 9000, - 200, - 500, - 4000, - 7500, - false - ); - - vm.expectEmit(true, false, false, false); - emit SetInterestRateModel(address(newIR)); - - vm.prank(CONFIGURATOR); - pool.updateInterestRateModel(address(newIR)); - - assertEq(address(pool.interestRateModel()), address(newIR), "Interest rate model was not set correctly"); - - // Add elUpdate - - vm.prank(CONFIGURATOR); - pool.updateInterestRateModel(address(newIR)); - - assertEq( - newIR.calcBorrowRate(expectedLiquidity, availableLiquidity), pool.borrowRate(), "Borrow rate does not match" - ); - } - - // [P4-23]: connectPoolQuotaManager updates quotaRevenue and emits event - - function test_P4_23_connectPoolQuotaManager_updates_quotaRevenue_and_emits_event() public { - pool = new Pool4626({ - _addressProvider: address(psts.addressProvider()), - _underlyingToken: tokenTestSuite.addressOf(Tokens.DAI), - _interestRateModel: address(irm), - _expectedLiquidityLimit: type(uint256).max, - _supportsQuotas: true - }); - - pqk = new PoolQuotaKeeper(address(pool)); - - address POOL_QUOTA_KEEPER = address(pqk); - - vm.expectEmit(true, true, false, false); - emit SetPoolQuotaKeeper(POOL_QUOTA_KEEPER); - - vm.prank(CONFIGURATOR); - pool.connectPoolQuotaManager(POOL_QUOTA_KEEPER); - - uint96 qu = uint96(WAD * 10); - - assertEq(pool.poolQuotaKeeper(), POOL_QUOTA_KEEPER, "Incorrect Pool QuotaKeeper"); - - vm.prank(POOL_QUOTA_KEEPER); - pool.updateQuotaRevenue(qu); - - uint256 year = 365 days; - - vm.warp(block.timestamp + year); - - PoolQuotaKeeper pqk2 = new PoolQuotaKeeper(address(pool)); - - address POOL_QUOTA_KEEPER2 = address(pqk2); - - vm.expectEmit(true, true, false, false); - emit SetPoolQuotaKeeper(POOL_QUOTA_KEEPER2); - - vm.prank(CONFIGURATOR); - pool.connectPoolQuotaManager(POOL_QUOTA_KEEPER2); - - assertEq(pool.lastQuotaRevenueUpdate(), block.timestamp, "Incorrect lastQuotaRevenuUpdate"); - assertEq(pool.quotaRevenue(), qu, "#1: Incorrect quotaRevenue"); - - assertEq(pool.expectedLiquidityLU(), qu / PERCENTAGE_FACTOR, "Incorrect expectedLiquidityLU"); - } - - // [P4-24]: setExpectedLiquidityLimit() sets limit & emits event - function test_P4_24_setExpectedLiquidityLimit_correct_and_emits_event() public { - vm.expectEmit(false, false, false, true); - emit SetExpectedLiquidityLimit(10005); - - vm.prank(CONFIGURATOR); - pool.setExpectedLiquidityLimit(10005); - - assertEq(pool.expectedLiquidityLimit(), 10005, "expectedLiquidityLimit not set correctly"); - } - - // [P4-25]: setTotalBorrowedLimit sets limit & emits event - function test_P4_25_setTotalBorrowedLimit_correct_and_emits_event() public { - vm.expectEmit(false, false, false, true); - emit SetTotalBorrowedLimit(10005); - - vm.prank(CONFIGURATOR); - pool.setTotalBorrowedLimit(10005); - - assertEq(pool.totalBorrowedLimit(), 10005, "totalBorrowedLimit not set correctly"); - } - - // [P4-26]: setWithdrawFee works correctly - function test_P4_26_setWithdrawFee_works_correctly() public { - vm.expectRevert(IncorrectParameterException.selector); - - vm.prank(CONFIGURATOR); - pool.setWithdrawFee(101); - - vm.expectEmit(false, false, false, true); - emit SetWithdrawFee(50); - - vm.prank(CONFIGURATOR); - pool.setWithdrawFee(50); - - assertEq(pool.withdrawFee(), 50, "withdrawFee not set correctly"); - } - - struct CreditManagerBorrowTestCase { - string name; - /// SETUP - uint16 u2; - bool isBorrowingMoreU2Forbidden; - uint256 borrowBefore1; - uint256 borrowBefore2; - /// PARAMS - uint256 totalBorrowLimit; - uint256 cmBorrowLimit; - /// EXPECTED VALUES - uint256 expectedCanBorrow; - } - - // [P4-27]: creditManagerCanBorrow computes availabel borrow correctly - function test_P4_27_creditManagerCanBorrow_computes_available_borrow_amount_correctly() public { - uint256 initialLiquidity = 10 * addLiquidity; - CreditManagerBorrowTestCase[5] memory cases = [ - CreditManagerBorrowTestCase({ - name: "Non-limit linear model, totalBorrowed > totalLimit", - // POOL SETUP - u2: 9000, - isBorrowingMoreU2Forbidden: false, - borrowBefore1: addLiquidity, - borrowBefore2: addLiquidity, - totalBorrowLimit: addLiquidity, - cmBorrowLimit: 5 * addLiquidity, - /// EXPECTED VALUES - expectedCanBorrow: 0 - }), - CreditManagerBorrowTestCase({ - name: "Non-limit linear model, cmBorrowLimit < totalLimit", - // POOL SETUP - u2: 9000, - isBorrowingMoreU2Forbidden: false, - borrowBefore1: addLiquidity, - borrowBefore2: addLiquidity, - totalBorrowLimit: 10 * addLiquidity, - cmBorrowLimit: 5 * addLiquidity, - /// EXPECTED VALUES - expectedCanBorrow: 4 * addLiquidity - }), - CreditManagerBorrowTestCase({ - name: "Non-limit linear model, cmBorrowLimit > totalLimit", - // POOL SETUP - u2: 9000, - isBorrowingMoreU2Forbidden: false, - borrowBefore1: addLiquidity, - borrowBefore2: addLiquidity, - totalBorrowLimit: 4 * addLiquidity, - cmBorrowLimit: 5 * addLiquidity, - /// EXPECTED VALUES - expectedCanBorrow: 2 * addLiquidity - }), - CreditManagerBorrowTestCase({ - name: "Limit linear model", - // POOL SETUP - u2: 6000, - isBorrowingMoreU2Forbidden: true, - borrowBefore1: 4 * addLiquidity, - borrowBefore2: addLiquidity, - totalBorrowLimit: 8 * addLiquidity, - cmBorrowLimit: 5 * addLiquidity, - /// EXPECTED VALUES - expectedCanBorrow: addLiquidity - }), - CreditManagerBorrowTestCase({ - name: "Non-limit linear model, cmBorrowed < cmBorrowLimit", - // POOL SETUP - u2: 9000, - isBorrowingMoreU2Forbidden: false, - borrowBefore1: addLiquidity, - borrowBefore2: 5 * addLiquidity, - totalBorrowLimit: 10 * addLiquidity, - cmBorrowLimit: addLiquidity, - /// EXPECTED VALUES - expectedCanBorrow: 0 - }) - ]; - - for (uint256 i; i < cases.length; ++i) { - CreditManagerBorrowTestCase memory testCase = cases[i]; - - _setUp(Tokens.DAI, false); - - _initPoolLiquidity(initialLiquidity, RAY); - - LinearInterestRateModel newIR = new LinearInterestRateModel( - 5000, - testCase.u2, - 200, - 500, - 4000, - 7500, - testCase.isBorrowingMoreU2Forbidden - ); - - CreditManagerMockForPoolTest cmMock2 = new CreditManagerMockForPoolTest( - address(pool) - ); - - vm.startPrank(CONFIGURATOR); - psts.cr().addCreditManager(address(cmMock2)); - - pool.updateInterestRateModel(address(newIR)); - pool.setTotalBorrowedLimit(type(uint256).max); - pool.setCreditManagerLimit(address(cmMock), type(uint128).max); - pool.setCreditManagerLimit(address(cmMock2), type(uint128).max); - - cmMock.lendCreditAccount(testCase.borrowBefore1, DUMB_ADDRESS); - cmMock2.lendCreditAccount(testCase.borrowBefore2, DUMB_ADDRESS); - - pool.setTotalBorrowedLimit(testCase.totalBorrowLimit); - - pool.setCreditManagerLimit(address(cmMock2), testCase.cmBorrowLimit); - - vm.stopPrank(); - - assertEq( - pool.creditManagerCanBorrow(address(cmMock2)), - testCase.expectedCanBorrow, - _testCaseErr(testCase.name, "Incorrect creditManagerCanBorrow return value") - ); - } - } - - struct SupplyRateTestCase { - string name; - /// SETUP - uint256 initialLiquidity; - uint16 utilisation; - uint16 withdrawFee; - // supportQuotas is true of quotaRevenue >0 - uint128 quotaRevenue; - uint256 expectedSupplyRate; - } - - // [P4-28]: supplyRate computes rates correctly - function test_P4_28_supplyRate_computes_rates_correctly() public { - SupplyRateTestCase[5] memory cases = [ - SupplyRateTestCase({ - name: "normal pool with zero debt and zero supply", - /// SETUP - initialLiquidity: 0, - utilisation: 0, - withdrawFee: 0, - quotaRevenue: 0, - expectedSupplyRate: irm.calcBorrowRate(0, 0, false) - }), - SupplyRateTestCase({ - name: "normal pool with zero debt and non-zero supply", - /// SETUP - initialLiquidity: addLiquidity, - utilisation: 0, - withdrawFee: 0, - quotaRevenue: 0, - expectedSupplyRate: 0 - }), - SupplyRateTestCase({ - name: "normal pool with 50% utilisation debt", - /// SETUP - initialLiquidity: addLiquidity, - utilisation: 50_00, - withdrawFee: 0, - quotaRevenue: 0, - // borrow rate will be distributed to all LPs (dieselRate =1), so supply is a half - expectedSupplyRate: irm.calcBorrowRate(200, 100, false) / 2 - }), - SupplyRateTestCase({ - name: "normal pool with 50% utilisation debt and withdrawFee", - /// SETUP - initialLiquidity: addLiquidity, - utilisation: 50_00, - withdrawFee: 50, - quotaRevenue: 0, - // borrow rate will be distributed to all LPs (dieselRate =1), so supply is a half and -1% for withdrawFee - expectedSupplyRate: ((irm.calcBorrowRate(200, 100, false) / 2) * 995) / 1000 - }), - SupplyRateTestCase({ - name: "normal pool with 50% utilisation debt, withdrawFee and quotas", - /// SETUP - initialLiquidity: addLiquidity, - utilisation: 50_00, - withdrawFee: 1_00, - quotaRevenue: uint128(addLiquidity) * 45_50, - // borrow rate will be distributed to all LPs (dieselRate =1), so supply is a half and -1% for withdrawFee - expectedSupplyRate: (((irm.calcBorrowRate(200, 100, false) / 2 + (45_50 * RAY) / PERCENTAGE_FACTOR)) * 99) / 100 - }) - ]; - - for (uint256 i; i < cases.length; ++i) { - SupplyRateTestCase memory testCase = cases[i]; - - bool supportQuotas = testCase.quotaRevenue > 0; - _setUpTestCase( - Tokens.DAI, 0, testCase.utilisation, testCase.initialLiquidity, RAY, testCase.withdrawFee, supportQuotas - ); - - if (supportQuotas) { - address POOL_QUOTA_KEEPER = address(pqk); - - vm.prank(CONFIGURATOR); - pool.connectPoolQuotaManager(POOL_QUOTA_KEEPER); - - vm.prank(CONFIGURATOR); - pool.connectPoolQuotaManager(POOL_QUOTA_KEEPER); - - vm.prank(POOL_QUOTA_KEEPER); - pool.updateQuotaRevenue(testCase.quotaRevenue); - } - - assertEq( - pool.supplyRate(), testCase.expectedSupplyRate, _testCaseErr(testCase.name, "Incorrect supply rate") - ); - - if (pool.totalSupply() > 0) { - uint256 depositAmount = addLiquidity / 10; - uint256 sharesGot = pool.previewDeposit(depositAmount); - - vm.warp(block.timestamp + 365 days); - - uint256 depositInAYear = pool.previewRedeem(sharesGot); - - assertEq( - pool.supplyRate(), testCase.expectedSupplyRate, _testCaseErr(testCase.name, "Incorrect supply rate") - ); - - uint256 expectedDepositInAYear = (depositAmount * (PERCENTAGE_FACTOR - testCase.withdrawFee)) - / PERCENTAGE_FACTOR + (depositAmount * testCase.expectedSupplyRate) / RAY; - - assertEq( - depositInAYear, expectedDepositInAYear, _testCaseErr(testCase.name, "Incorrect deposit growth") - ); - } - } - } - - // 10000000000000000 - // // [P4-23]: fromDiesel / toDiesel works correctly - // function test_PX_23_diesel_conversion_is_correct() public { - // _connectAndSetLimit(); - - // vm.prank(USER); - // pool.deposit(addLiquidity, USER); - - // address ca = cmMock.getCreditAccountOrRevert(DUMB_ADDRESS); - - // cmMock.lendCreditAccount(addLiquidity / 2, ca); - - // uint256 timeWarp = 365 days; - - // vm.warp(block.timestamp + timeWarp); - - // uint256 dieselRate = pool.getDieselRate_RAY(); - - // assertEq( - // pool.convertToShares(addLiquidity), (addLiquidity * RAY) / dieselRate, "ToDiesel does not compute correctly" - // ); - - // assertEq( - // pool.convertToAssets(addLiquidity), (addLiquidity * dieselRate) / RAY, "ToDiesel does not compute correctly" - // ); - // } - - // // [P4-28]: expectedLiquidity() computes correctly - // function test_PX_28_expectedLiquidity_correct() public { - // _connectAndSetLimit(); - - // vm.prank(USER); - // pool.deposit(addLiquidity, USER); - - // address ca = cmMock.getCreditAccountOrRevert(DUMB_ADDRESS); - - // cmMock.lendCreditAccount(addLiquidity / 2, ca); - - // uint256 borrowRate = pool.borrowRate(); - // uint256 timeWarp = 365 days; - - // vm.warp(block.timestamp + timeWarp); - - // uint256 expectedInterest = ((addLiquidity / 2) * borrowRate) / RAY; - // uint256 expectedLiquidity = pool.expectedLiquidityLU() + expectedInterest; - - // assertEq(pool.expectedLiquidity(), expectedLiquidity, "Index value was not updated correctly"); - // } - - // // [P4-35]: updateInterestRateModel reverts on zero address - // function test_PX_35_updateInterestRateModel_reverts_on_zero_address() public { - // vm.expectRevert(ZeroAddressException.selector); - // vm.prank(CONFIGURATOR); - // pool.updateInterestRateModel(address(0)); - // } -} diff --git a/contracts/test/interfaces/ICreditConfig.sol b/contracts/test/interfaces/ICreditConfig.sol index fef485f9..47fb5be2 100644 --- a/contracts/test/interfaces/ICreditConfig.sol +++ b/contracts/test/interfaces/ICreditConfig.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox. Generalized leverage protocol that allows to take leverage and then use it across other DeFi protocols and platforms in a composable way. // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {ITokenTestSuite} from "./ITokenTestSuite.sol"; import {PriceFeedConfig} from "@gearbox-protocol/core-v2/contracts/oracles/PriceOracle.sol"; diff --git a/contracts/test/interfaces/ITokenTestSuite.sol b/contracts/test/interfaces/ITokenTestSuite.sol index 199779ff..e0515249 100644 --- a/contracts/test/interfaces/ITokenTestSuite.sol +++ b/contracts/test/interfaces/ITokenTestSuite.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox. Generalized leverage protocol that allows to take leverage and then use it across other DeFi protocols and platforms in a composable way. // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; interface ITokenTestSuite { function wethToken() external view returns (address); diff --git a/contracts/test/lib/helper.sol b/contracts/test/lib/helper.sol index 94bc0f22..54789f12 100644 --- a/contracts/test/lib/helper.sol +++ b/contracts/test/lib/helper.sol @@ -4,7 +4,30 @@ pragma solidity ^0.8.17; import "./constants.sol"; import {Test} from "forge-std/Test.sol"; +struct VarU256 { + mapping(bytes32 => uint256) values; + mapping(bytes32 => bool) isSet; +} + +library Vars { + function set(VarU256 storage v, string memory key, uint256 value) internal { + bytes32 b32key = keccak256(bytes(key)); + v.values[b32key] = value; + v.isSet[b32key] = true; + } + + function get(VarU256 storage v, string memory key) internal view returns (uint256) { + bytes32 b32key = keccak256(bytes(key)); + require(v.isSet[b32key], string.concat("Value ", key, " is undefined")); + return v.values[b32key]; + } +} + contract TestHelper is Test { + VarU256 internal vars; + + string caseName; + constructor() { vm.label(USER, "USER"); vm.label(FRIEND, "FRIEND"); @@ -14,7 +37,80 @@ contract TestHelper is Test { vm.label(ADAPTER, "ADAPTER"); } - function _testCaseErr(string memory caseName, string memory err) internal pure returns (string memory) { - return string.concat("\nCase: ", caseName, "\nError: ", err); + function _testCaseErr(string memory err) internal view returns (string memory) { + return _testCaseErr(caseName, err); + } + + function _testCaseErr(string memory _caseName, string memory err) internal pure returns (string memory) { + return string.concat("\nCase: ", _caseName, "\nError: ", err); + } + + function arrayOf(uint256 v1) internal pure returns (uint256[] memory array) { + array = new uint256[](1); + array[0] = v1; + } + + function arrayOf(uint256 v1, uint256 v2) internal pure returns (uint256[] memory array) { + array = new uint256[](2); + array[0] = v1; + array[1] = v2; + } + + function arrayOf(uint256 v1, uint256 v2, uint256 v3) internal pure returns (uint256[] memory array) { + array = new uint256[](3); + array[0] = v1; + array[1] = v2; + array[2] = v3; + } + + function arrayOf(uint256 v1, uint256 v2, uint256 v3, uint256 v4) internal pure returns (uint256[] memory array) { + array = new uint256[](4); + array[0] = v1; + array[1] = v2; + array[2] = v3; + array[3] = v4; + } + + function arrayOfU16(uint16 v1) internal pure returns (uint16[] memory array) { + array = new uint16[](1); + array[0] = v1; + } + + function arrayOfU16(uint16 v1, uint16 v2) internal pure returns (uint16[] memory array) { + array = new uint16[](2); + array[0] = v1; + array[1] = v2; + } + + function arrayOfU16(uint16 v1, uint16 v2, uint16 v3) internal pure returns (uint16[] memory array) { + array = new uint16[](3); + array[0] = v1; + array[1] = v2; + array[2] = v3; + } + + function arrayOfU16(uint16 v1, uint16 v2, uint16 v3, uint16 v4) internal pure returns (uint16[] memory array) { + array = new uint16[](4); + array[0] = v1; + array[1] = v2; + array[2] = v3; + array[3] = v4; + } + + function _copyU16toU256(uint16[] memory a16) internal pure returns (uint256[] memory a256) { + uint256 len = a16.length; + uint256[] memory a256 = new uint256[](len); + + unchecked { + for (uint256 i; i < len; ++i) { + a256[i] = a16[i]; + } + } + } + + function assertEq(uint16[] memory a1, uint16[] memory a2, string memory reason) internal { + assertEq(a1.length, a2.length, string.concat(reason, "Arrays has different length")); + + assertEq(_copyU16toU256(a1), _copyU16toU256(a2), reason); } } diff --git a/contracts/test/mocks/GeneralMock.sol b/contracts/test/mocks/GeneralMock.sol index c4d90511..2572942c 100644 --- a/contracts/test/mocks/GeneralMock.sol +++ b/contracts/test/mocks/GeneralMock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; contract GeneralMock { bytes public data; @@ -9,4 +9,6 @@ contract GeneralMock { fallback() external { data = msg.data; } + + receive() external payable {} } diff --git a/contracts/test/mocks/core/ACLTraitTest.sol b/contracts/test/mocks/core/ACLTraitTest.sol index c322bcb6..71967430 100644 --- a/contracts/test/mocks/core/ACLTraitTest.sol +++ b/contracts/test/mocks/core/ACLTraitTest.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {ACLNonReentrantTrait} from "../../../traits/ACLNonReentrantTrait.sol"; diff --git a/contracts/test/mocks/core/AccountFactoryMock.sol b/contracts/test/mocks/core/AccountFactoryMock.sol new file mode 100644 index 00000000..cdfc97e6 --- /dev/null +++ b/contracts/test/mocks/core/AccountFactoryMock.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; +//pragma abicoder v1; + +import {IAccountFactory} from "../../../interfaces/IAccountFactory.sol"; +import {CreditAccountMock} from "../credit/CreditAccountMock.sol"; + +// EXCEPTIONS + +import {Test} from "forge-std/Test.sol"; +import "forge-std/console.sol"; + +/// @title Disposable credit accounts factory +contract AccountFactoryMock is Test, IAccountFactory { + /// @dev Contract version + uint256 public version; + + address public usedAccount; + + address public returnedAccount; + + constructor(uint256 _version) { + usedAccount = address(new CreditAccountMock()); + + version = _version; + + vm.label(usedAccount, "CREDIT_ACCOUNT"); + } + + /// @dev Provides a new credit account to a Credit Manager + /// @return creditAccount Address of credit account + function takeCreditAccount(uint256, uint256) external view override returns (address creditAccount) { + return usedAccount; + } + + function returnCreditAccount(address _usedAccount) external override { + returnedAccount = _usedAccount; + } +} diff --git a/contracts/test/mocks/core/AddressProviderACLMock.sol b/contracts/test/mocks/core/AddressProviderACLMock.sol deleted file mode 100644 index 9403f76b..00000000 --- a/contracts/test/mocks/core/AddressProviderACLMock.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; - -import "../../lib/constants.sol"; - -/** - * @title Address Provider that returns ACL and isConfigurator - * @notice this contract is used to test LPPriceFeeds - */ -contract AddressProviderACLMock { - address public getACL; - mapping(address => bool) public isConfigurator; - - address public getPriceOracle; - mapping(address => address) public priceFeeds; - - address public getTreasuryContract; - - address public owner; - - address public getGearToken; - - constructor() { - getACL = address(this); - getPriceOracle = address(this); - getTreasuryContract = FRIEND2; - isConfigurator[msg.sender] = true; - owner = msg.sender; - } - - function setPriceFeed(address token, address feed) external { - priceFeeds[token] = feed; - } - - function setGearToken(address gearToken) external { - getGearToken = gearToken; - } - - receive() external payable {} -} diff --git a/contracts/test/mocks/core/AddressProviderV3ACLMock.sol b/contracts/test/mocks/core/AddressProviderV3ACLMock.sol new file mode 100644 index 00000000..1af26e3b --- /dev/null +++ b/contracts/test/mocks/core/AddressProviderV3ACLMock.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +import "../../../core/AddressProviderV3.sol"; +import {AccountFactoryMock} from "../core/AccountFactoryMock.sol"; +import {PriceOracleMock} from "../oracles/PriceOracleMock.sol"; +import {WETHGatewayMock} from "../support/WETHGatewayMock.sol"; +import {WithdrawalManagerMock} from "../support/WithdrawalManagerMock.sol"; +import {BotListMock} from "../support/BotListMock.sol"; +import "../../lib/constants.sol"; +import {Test} from "forge-std/Test.sol"; + +import "forge-std/console.sol"; + +/// +/// @title Address Provider that returns ACL and isConfigurator + +contract AddressProviderV3ACLMock is Test, AddressProviderV3 { + address public owner; + mapping(address => bool) public isConfigurator; + + mapping(address => bool) public isPool; + mapping(address => bool) public isCreditManager; + + mapping(address => bool) public isPausableAdmin; + mapping(address => bool) public isUnpausableAdmin; + + constructor() AddressProviderV3(address(this)) { + PriceOracleMock priceOracleMock = new PriceOracleMock(); + _setAddress(AP_PRICE_ORACLE, address(priceOracleMock), priceOracleMock.version()); + + WETHGatewayMock wethGatewayMock = new WETHGatewayMock(); + _setAddress(AP_WETH_GATEWAY, address(wethGatewayMock), wethGatewayMock.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); + + BotListMock botListMock = new BotListMock(); + _setAddress(AP_BOT_LIST, address(botListMock), 3_00); + + _setAddress(AP_CONTRACTS_REGISTER, address(this), 1); + + _setAddress(AP_TREASURY, makeAddr("TREASURY"), 0); + + isConfigurator[msg.sender] = true; + owner = msg.sender; + } + + function addPool(address pool) external { + isPool[pool] = true; + } + + function addCreditManager(address creditManager) external { + isCreditManager[creditManager] = true; + } + + /// @dev Adds an address to the set of admins that can pause contracts + /// @param newAdmin Address of a new pausable admin + function addPausableAdmin(address newAdmin) external { + isPausableAdmin[newAdmin] = true; + } + + /// @dev Adds unpausable admin address to the list + /// @param newAdmin Address of new unpausable admin + function addUnpausableAdmin(address newAdmin) external { + isUnpausableAdmin[newAdmin] = true; + } + + function getAddressOrRevert(bytes32 key, uint256 _version) public view override returns (address result) { + result = addresses[key][_version]; + if (result == address(0)) { + string memory keyString = bytes32ToString(key); + console.log("AddressProviderV3: Cant find ", keyString, ", version:", _version); + } + + return super.getAddressOrRevert(key, _version); + } + + function bytes32ToString(bytes32 _bytes32) internal pure returns (string memory) { + uint8 i = 0; + while (i < 32 && _bytes32[i] != 0) { + i++; + } + bytes memory bytesArray = new bytes(i); + for (i = 0; i < 32 && _bytes32[i] != 0; i++) { + bytesArray[i] = _bytes32[i]; + } + return string(bytesArray); + } +} diff --git a/contracts/test/mocks/credit/CreditAccountMock.sol b/contracts/test/mocks/credit/CreditAccountMock.sol new file mode 100644 index 00000000..6323afd6 --- /dev/null +++ b/contracts/test/mocks/credit/CreditAccountMock.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; +pragma abicoder v1; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {ICreditAccount} from "../../../interfaces/ICreditAccount.sol"; + +interface CreditAccountMockEvents { + event TransferCall(address token, address to, uint256 amount); + + event ExecuteCall(address destination, bytes data); +} + +contract CreditAccountMock is ICreditAccount, CreditAccountMockEvents { + using Address for address; + + address public creditManager; + + // Contract version + uint256 public constant version = 3_00; + + bytes public return_executeResult; + + mapping(address => uint8) public revertsOnTransfer; + + function setRevertOnTransfer(address token, uint8 times) external { + revertsOnTransfer[token] = times; + } + + function safeTransfer(address token, address to, uint256 amount) external { + if (revertsOnTransfer[token] > 0) { + revertsOnTransfer[token]--; + revert("Token transfer reverted"); + } + + if (token.isContract()) IERC20(token).transfer(to, amount); + emit TransferCall(token, to, amount); + } + + function execute(address destination, bytes memory data) external returns (bytes memory) { + emit ExecuteCall(destination, data); + return return_executeResult; + } + + function setReturnExecuteResult(bytes calldata _result) external { + return_executeResult = _result; + } +} diff --git a/contracts/test/mocks/credit/CreditManagerMock.sol b/contracts/test/mocks/credit/CreditManagerMock.sol new file mode 100644 index 00000000..8822c0eb --- /dev/null +++ b/contracts/test/mocks/credit/CreditManagerMock.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; +pragma abicoder v1; + +import "../../../interfaces/IAddressProviderV3.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {ICreditManagerV3} from "../../../interfaces/ICreditManagerV3.sol"; +import {IPoolV3} from "../../../interfaces/IPoolV3.sol"; +import {IPoolQuotaKeeper} from "../../../interfaces/IPoolQuotaKeeper.sol"; + +contract CreditManagerMock { + /// @dev Factory contract for Credit Accounts + address public addressProvider; + + /// @dev Address of the underlying asset + address public underlying; + + /// @dev Address of the connected pool + address public poolService; + address public pool; + + /// @dev Address of WETH + address public weth; + + /// @dev Address of WETH Gateway + address public wethGateway; + + mapping(address => uint256) public getTokenMaskOrRevert; + + constructor(address _addressProvider, address _pool) { + addressProvider = _addressProvider; + weth = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_WETH_TOKEN, NO_VERSION_CONTROL); // U:[CM-1] + wethGateway = IAddressProviderV3(addressProvider).getAddressOrRevert(AP_WETH_GATEWAY, 3_00); // U:[CM-1] + setPoolService(_pool); + } + + function setPoolService(address newPool) public { + poolService = newPool; + pool = newPool; + } + + /// @notice Outdated + function lendCreditAccount(uint256 borrowedAmount, address ca) external { + IPoolV3(poolService).lendCreditAccount(borrowedAmount, ca); + } + + /// @notice Outdated + function repayCreditAccount(uint256 borrowedAmount, uint256 profit, uint256 loss) external { + IPoolV3(poolService).repayCreditAccount(borrowedAmount, profit, loss); + } + + /// @notice Outdated + function updateQuota(address _creditAccount, address token, int96 quotaChange) + external + returns (uint256 caQuotaInterestChange, bool tokensToEnable, uint256 tokensToDisable) + { + (caQuotaInterestChange,,) = + IPoolQuotaKeeper(IPoolV3(pool).poolQuotaKeeper()).updateQuota(_creditAccount, token, quotaChange); + } + + function addToken(address token, uint256 mask) external { + getTokenMaskOrRevert[token] = mask; + } +} diff --git a/contracts/test/mocks/credit/CreditManagerTestInternal.sol b/contracts/test/mocks/credit/CreditManagerTestInternal.sol deleted file mode 100644 index b8e60596..00000000 --- a/contracts/test/mocks/credit/CreditManagerTestInternal.sol +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; - -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {CreditManagerV3, ClosureAction} from "../../../credit/CreditManagerV3.sol"; -import {IPriceOracleV2} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; -import {IPoolQuotaKeeper} from "../../../interfaces/IPoolQuotaKeeper.sol"; -import {CollateralTokenData} from "../../../interfaces/ICreditManagerV3.sol"; - -// EXCEPTIONS -import "../../../interfaces/IExceptions.sol"; - -/// @title Credit Manager Internal -/// @notice It encapsulates business logic for managing credit accounts -/// -/// More info: https://dev.gearbox.fi/developers/credit/credit_manager -contract CreditManagerTestInternal is CreditManagerV3 { - using SafeERC20 for IERC20; - using Address for address payable; - - /// @dev Constructor - /// @param _poolService Address of pool service - constructor(address _poolService, address _withdrawalManager) CreditManagerV3(_poolService, _withdrawalManager) {} - - function setCumulativeDropAtFastCheck(address creditAccount, uint16 value) external { - // cumulativeDropAtFastCheckRAY[creditAccount] = value; - } - - // function calcNewCumulativeIndex( - // uint256 borrowedAmount, - // uint256 delta, - // uint256 cumulativeIndexNow, - // uint256 cumulativeIndexOpen, - // bool isIncrease - // ) external pure returns (uint256 newCumulativeIndex) { - // newCumulativeIndex = - // _calcNewCumulativeIndex(borrowedAmount, delta, cumulativeIndexNow, cumulativeIndexOpen, isIncrease); - // } - - // function calcClosePaymentsPure( - // uint256 totalValue, - // ClosureAction closureActionType, - // uint256 borrowedAmount, - // uint256 borrowedAmountWithInterest - // ) external view returns (uint256 amountToPool, uint256 remainingFunds, uint256 profit, uint256 loss) { - // return calcClosePayments(totalValue, closureActionType, borrowedAmount, borrowedAmountWithInterest); - // } - - function transferAssetsTo(address creditAccount, address to, bool convertWETH, uint256 enabledTokensMask) - external - { - _transferAssetsTo(creditAccount, to, convertWETH, enabledTokensMask); - } - - function safeTokenTransfer(address creditAccount, address token, address to, uint256 amount, bool convertToETH) - external - { - _safeTokenTransfer(creditAccount, token, to, amount, convertToETH); - } - - // function disableToken(address creditAccount, address token) external override { - // _disableToken(creditAccount, token); - // } - - function getCreditAccountParameters(address creditAccount) - external - view - returns (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow) - { - return _getCreditAccountParameters(creditAccount); - } - - function collateralTokensInternal() external view returns (address[] memory collateralTokensAddr) { - uint256 len = collateralTokensCount; - collateralTokensAddr = new address[](len); - for (uint256 i = 0; i < len; i++) { - (collateralTokensAddr[i],) = collateralTokens(i); - } - } - - function collateralTokensDataExt(uint256 tokenMask) external view returns (CollateralTokenData memory) { - return collateralTokensData[tokenMask]; - } - - function setenabledTokensMask(address creditAccount, uint256 enabledTokensMask) external { - creditAccountInfo[creditAccount].enabledTokensMask = uint248(enabledTokensMask); - } - - function getSlotBytes(uint256 slotNum) external view returns (bytes32 slotVal) { - assembly { - slotVal := sload(slotNum) - } - } -} diff --git a/contracts/test/mocks/oracles/PriceOracleMock.sol b/contracts/test/mocks/oracles/PriceOracleMock.sol new file mode 100644 index 00000000..06aead6d --- /dev/null +++ b/contracts/test/mocks/oracles/PriceOracleMock.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; +//pragma abicoder v1; + +import {IPriceOracleV2} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; + +// EXCEPTIONS + +import {Test} from "forge-std/Test.sol"; +import "forge-std/console.sol"; + +/// @title Disposable credit accounts factory +contract PriceOracleMock is Test, IPriceOracleV2 { + mapping(address => uint256) public priceInUSD; + + uint256 public constant override version = 2; + + constructor() { + vm.label(address(this), "PRICE_ORACLE"); + } + + function setPrice(address token, uint256 price) external { + priceInUSD[token] = price; + } + + /// @dev Converts a quantity of an asset to USD (decimals = 8). + /// @param amount Amount to convert + /// @param token Address of the token to be converted + function convertToUSD(uint256 amount, address token) public view returns (uint256) { + return amount * getPrice(token) / 10 ** 8; + } + + /// @dev Converts a quantity of USD (decimals = 8) to an equivalent amount of an asset + /// @param amount Amount to convert + /// @param token Address of the token converted to + function convertFromUSD(uint256 amount, address token) public view returns (uint256) { + return amount * 10 ** 8 / getPrice(token); + } + + /// @dev Converts one asset into another + /// + /// @param amount Amount to convert + /// @param tokenFrom Address of the token to convert from + /// @param tokenTo Address of the token to convert to + function convert(uint256 amount, address tokenFrom, address tokenTo) external view returns (uint256) { + return convertFromUSD(convertToUSD(amount, tokenFrom), tokenTo); + } + + /// @dev Returns token's price in USD (8 decimals) + /// @param token The token to compute the price for + function getPrice(address token) public view returns (uint256 price) { + price = priceInUSD[token]; + if (price == 0) revert("Price is not set"); + } + + /// @dev Returns the price feed address for the passed token + /// @param token Token to get the price feed for + function priceFeeds(address token) external view returns (address priceFeed) {} + + /// @dev Returns the price feed for the passed token, + /// with additional parameters + /// @param token Token to get the price feed for + function priceFeedsWithFlags(address token) + external + view + returns (address priceFeed, bool skipCheck, uint256 decimals) + {} + + /// OUTDATED! + function fastCheck(uint256 amountFrom, address tokenFrom, uint256 amountTo, address tokenTo) + external + view + returns (uint256 collateralFrom, uint256 collateralTo) + {} +} diff --git a/contracts/test/mocks/pool/CreditManagerMockForPoolTest.sol b/contracts/test/mocks/pool/CreditManagerMockForPoolTest.sol deleted file mode 100644 index a1a7a608..00000000 --- a/contracts/test/mocks/pool/CreditManagerMockForPoolTest.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; - -import {IPool4626} from "../../../interfaces/IPool4626.sol"; -import {IPoolQuotaKeeper} from "../../../interfaces/IPoolQuotaKeeper.sol"; -import "../../lib/constants.sol"; - -contract CreditManagerMockForPoolTest { - address public poolService; - address public pool; - address public underlying; - - address public creditAccount = DUMB_ADDRESS; - - mapping(address => uint256) public getTokenMaskOrRevert; - - constructor(address _poolService) { - changePoolService(_poolService); - } - - function changePoolService(address newPool) public { - poolService = newPool; - pool = newPool; - } - - /** - * @dev Transfers money from the pool to credit account - * and updates the pool parameters - * @param borrowedAmount Borrowed amount for credit account - * @param ca Credit account address - */ - function lendCreditAccount(uint256 borrowedAmount, address ca) external { - IPool4626(poolService).lendCreditAccount(borrowedAmount, ca); - } - - /** - * @dev Recalculates total borrowed & borrowRate - * mints/burns diesel tokens - */ - function repayCreditAccount(uint256 borrowedAmount, uint256 profit, uint256 loss) external { - IPool4626(poolService).repayCreditAccount(borrowedAmount, profit, loss); - } - - function getCreditAccountOrRevert(address) public view returns (address result) { - result = creditAccount; - } - - function updateQuota(address _creditAccount, address token, int96 quotaChange) - external - returns (uint256 caQuotaInterestChange, bool tokensToEnable, uint256 tokensToDisable) - { - (caQuotaInterestChange,,) = - IPoolQuotaKeeper(IPool4626(pool).poolQuotaKeeper()).updateQuota(_creditAccount, token, quotaChange); - } - - function addToken(address token, uint256 mask) external { - getTokenMaskOrRevert[token] = mask; - } -} diff --git a/contracts/test/mocks/pool/GaugeMock.sol b/contracts/test/mocks/pool/GaugeMock.sol index 667516c1..103fed3c 100644 --- a/contracts/test/mocks/pool/GaugeMock.sol +++ b/contracts/test/mocks/pool/GaugeMock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -21,7 +21,7 @@ import {IGearStaking} from "../../../interfaces/IGearStaking.sol"; import {RAY, SECONDS_PER_YEAR, MAX_WITHDRAW_FEE} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; -import {Pool4626} from "../../../pool/Pool4626.sol"; +import {PoolV3} from "../../../pool/PoolV3.sol"; import "forge-std/console.sol"; @@ -34,7 +34,7 @@ contract GaugeMock is ACLNonReentrantTrait { address public immutable addressProvider; /// @dev Address of the pool - Pool4626 public immutable pool; + PoolV3 public immutable pool; /// @dev Mapping from token address to its rate parameters mapping(address => uint16) public rates; @@ -45,9 +45,9 @@ contract GaugeMock is ACLNonReentrantTrait { /// @dev Constructor - constructor(address _pool) ACLNonReentrantTrait(address(Pool4626(_pool).addressProvider())) nonZeroAddress(_pool) { - addressProvider = address(Pool4626(_pool).addressProvider()); // F:[P4-01] - pool = Pool4626(payable(_pool)); // F:[P4-01] + constructor(address _pool) ACLNonReentrantTrait(address(PoolV3(_pool).addressProvider())) nonZeroAddress(_pool) { + addressProvider = address(PoolV3(_pool).addressProvider()); // F:[P4-01] + pool = PoolV3(payable(_pool)); // F:[P4-01] } /// @dev Rolls the new epoch and updates all quota rates diff --git a/contracts/test/mocks/pool/PoolServiceMock.sol b/contracts/test/mocks/pool/PoolMock.sol similarity index 96% rename from contracts/test/mocks/pool/PoolServiceMock.sol rename to contracts/test/mocks/pool/PoolMock.sol index f5f2ff70..4b64d69b 100644 --- a/contracts/test/mocks/pool/PoolServiceMock.sol +++ b/contracts/test/mocks/pool/PoolMock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -19,7 +19,7 @@ import "../../../interfaces/IExceptions.sol"; * @notice Used for testing purposes only. * @author Gearbox */ -contract PoolServiceMock is IPoolService { +contract PoolMock is IPoolService { using SafeERC20 for IERC20; // Address repository @@ -65,7 +65,7 @@ contract PoolServiceMock is IPoolService { uint256 public override _cumulativeIndex_RAY; // Contract version - uint256 public constant override version = 1; + uint256 public constant override version = 3_00; uint128 public quotaRevenue; @@ -96,7 +96,7 @@ contract PoolServiceMock is IPoolService { supportsQuotas = val; } - function setCumulative_RAY(uint256 cumulativeIndex_RAY) external { + function setCumulativeIndexNow(uint256 cumulativeIndex_RAY) external { _cumulativeIndex_RAY = cumulativeIndex_RAY; } @@ -218,7 +218,7 @@ contract PoolServiceMock is IPoolService { function setWithdrawFee(uint256 num) external {} - function connectPoolQuotaManager(address _poolQuotaKeeper) external { + function setPoolQuotaManager(address _poolQuotaKeeper) external { poolQuotaKeeper = _poolQuotaKeeper; } diff --git a/contracts/test/mocks/pool/PoolQuotaKeeperMock.sol b/contracts/test/mocks/pool/PoolQuotaKeeperMock.sol new file mode 100644 index 00000000..bd594aa7 --- /dev/null +++ b/contracts/test/mocks/pool/PoolQuotaKeeperMock.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +import {IPoolQuotaKeeper, TokenQuotaParams, AccountQuota} from "../../../interfaces/IPoolQuotaKeeper.sol"; +import "forge-std/console.sol"; + +contract PoolQuotaKeeperMock is IPoolQuotaKeeper { + uint256 public constant override version = 3_00; + + /// @dev Address provider + address public immutable underlying; + + /// @dev Address of the protocol treasury + address public immutable override pool; + + /// @dev Mapping from token address to its respective quota parameters + TokenQuotaParams public totalQuotaParam; + + /// @dev Mapping from creditAccount => token > quota parameters + AccountQuota public accountQuota; + + /// @dev Address of the gauge that determines quota rates + address public gauge; + + /// @dev Timestamp of the last time quota rates were batch-updated + uint40 public lastQuotaRateUpdate; + + /// MOCK functionality + address public call_creditAccount; + address public call_token; + int96 public call_quotaChange; + address[] public call_tokens; + bool public call_setLimitsToZero; + + /// + uint256 internal return_caQuotaInterestChange; + bool internal return_enableToken; + bool internal return_disableToken; + + uint256 internal return_quoted; + uint256 internal return_interest; + bool internal return_isQuotedToken; + + mapping(address => uint96) internal _quoted; + mapping(address => uint256) internal _outstandingInterest; + + constructor(address _pool, address _underlying) { + pool = _pool; + underlying = _underlying; + } + + function updateQuota(address creditAccount, address token, int96 quotaChange) + external + view + returns (uint256 caQuotaInterestChange, bool enableToken, bool disableToken) + { + caQuotaInterestChange = return_caQuotaInterestChange; + enableToken = return_enableToken; + disableToken = return_disableToken; + } + + function setUpdateQuotaReturns(uint256 caQuotaInterestChange, bool enableToken, bool disableToken) external { + return_caQuotaInterestChange = caQuotaInterestChange; + return_enableToken = enableToken; + return_disableToken = disableToken; + } + + /// @dev Updates all quotas to zero when closing a credit account, and computes the final quota interest change + /// @param creditAccount Address of the Credit Account being closed + /// @param tokens Array of all active quoted tokens on the account + function removeQuotas(address creditAccount, address[] memory tokens, bool setLimitsToZero) external { + call_creditAccount = creditAccount; + call_tokens = tokens; + call_setLimitsToZero = setLimitsToZero; + } + + /// @dev Computes the accrued quota interest and updates interest indexes + /// @param creditAccount Address of the Credit Account to accrue interest for + /// @param tokens Array of all active quoted tokens on the account + function accrueQuotaInterest(address creditAccount, address[] memory tokens) external {} + + /// @dev Gauge management + + /// @dev Registers a new quoted token in the keeper + function addQuotaToken(address token) external {} + + /// @dev Batch updates the quota rates and changes the combined quota revenue + function updateRates() external {} + + function setQuotaAndOutstandingInterest(address token, uint96 quoted, uint256 outstandingInterest) external { + _quoted[token] = quoted; + _outstandingInterest[token] = outstandingInterest; + } + + /// GETTERS + function getQuotaAndOutstandingInterest(address creditAccount, address token) + external + view + override + returns (uint256 quoted, uint256 interest) + { + quoted = _quoted[token]; + interest = _outstandingInterest[token]; + } + + /// @dev Returns cumulative index in RAY for a quoted token. Returns 0 for non-quoted tokens. + function cumulativeIndex(address token) public view override returns (uint192) { + // return totalQuotaParams[token].cumulativeIndexSince(lastQuotaRateUpdate); + } + + /// @dev Returns quota rate in PERCENTAGE FORMAT + function getQuotaRate(address token) external view override returns (uint16) { + return totalQuotaParam.rate; + } + + /// @dev Returns an array of all quoted tokens + function quotedTokens() external view override returns (address[] memory) { + // return quotaTokensSet.values(); + } + + /// @dev Returns whether a token is quoted + function isQuotedToken(address token) external view override returns (bool) { + return return_isQuotedToken; + } + + /// @dev Returns quota parameters for a single (account, token) pair + function getQuota(address creditAccount, address token) + external + view + returns (uint96 quota, uint192 cumulativeIndexLU) + { + AccountQuota storage aq = accountQuota; + return (aq.quota, aq.cumulativeIndexLU); + } +} diff --git a/contracts/test/mocks/support/BotListMock.sol b/contracts/test/mocks/support/BotListMock.sol new file mode 100644 index 00000000..9a412109 --- /dev/null +++ b/contracts/test/mocks/support/BotListMock.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +contract BotListMock {} diff --git a/contracts/test/mocks/support/PolicyManagerInternal.sol b/contracts/test/mocks/support/PolicyManagerInternal.sol index 95c52fc4..0f1f23a0 100644 --- a/contracts/test/mocks/support/PolicyManagerInternal.sol +++ b/contracts/test/mocks/support/PolicyManagerInternal.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {PolicyManager} from "../../../support/risk-controller/PolicyManager.sol"; diff --git a/contracts/test/mocks/support/WETHGatewayMock.sol b/contracts/test/mocks/support/WETHGatewayMock.sol new file mode 100644 index 00000000..ef059919 --- /dev/null +++ b/contracts/test/mocks/support/WETHGatewayMock.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; +pragma abicoder v1; + +/// @title WETHGatewayMock +contract WETHGatewayMock { + mapping(address => uint256) public balanceOf; + + uint256 public constant version = 3_00; + // CREDIT MANAGERS + + function depositFor(address to, uint256 amount) external { + balanceOf[to] += amount; + } + + function withdrawTo(address owner) external {} +} diff --git a/contracts/test/mocks/support/WithdrawalManagerMock.sol b/contracts/test/mocks/support/WithdrawalManagerMock.sol new file mode 100644 index 00000000..a9542d86 --- /dev/null +++ b/contracts/test/mocks/support/WithdrawalManagerMock.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; +pragma abicoder v1; + +struct CancellableWithdrawals { + address token; + uint256 amount; +} + +/// @title WithdrawalManagerMock +contract WithdrawalManagerMock { + uint256 public constant version = 3_00; + // // CREDIT MANAGERS + + uint40 public delay; + + mapping(bool => CancellableWithdrawals[2]) amoucancellableWithdrawals; + + function cancellableScheduledWithdrawals(address, bool isForceCancel) + external + view + returns (address token1, uint256 amount1, address token2, uint256 amount2) + { + CancellableWithdrawals[2] storage cw = amoucancellableWithdrawals[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 { + amoucancellableWithdrawals[isForceCancel][0] = CancellableWithdrawals({token: token1, amount: amount1}); + amoucancellableWithdrawals[isForceCancel][1] = CancellableWithdrawals({token: token2, amount: amount2}); + } + + function setDelay(uint40 _delay) external { + delay = _delay; + } +} diff --git a/contracts/test/mocks/token/DegenNFTMock.sol b/contracts/test/mocks/token/DegenNFTMock.sol new file mode 100644 index 00000000..fcca35be --- /dev/null +++ b/contracts/test/mocks/token/DegenNFTMock.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +contract DegenNFTMock {} diff --git a/contracts/test/mocks/token/ERC20Blacklistable.sol b/contracts/test/mocks/token/ERC20Blacklistable.sol index 00bfb431..77fd7410 100644 --- a/contracts/test/mocks/token/ERC20Blacklistable.sol +++ b/contracts/test/mocks/token/ERC20Blacklistable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/contracts/test/mocks/token/ERC20FeeMock.sol b/contracts/test/mocks/token/ERC20FeeMock.sol index 9f4f114e..cf2cb4e6 100644 --- a/contracts/test/mocks/token/ERC20FeeMock.sol +++ b/contracts/test/mocks/token/ERC20FeeMock.sol @@ -1,34 +1,28 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ERC20Mock} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/ERC20Mock.sol"; import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; -import {IUSDT} from "../../../interfaces/external/IUSDT.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract ERC20FeeMock is IUSDT, ERC20Mock { - uint256 public override basisPointsRate; - uint256 public override maximumFee; +contract ERC20FeeMock is ERC20Mock { + uint256 public basisPointsRate; + uint256 public maximumFee; constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20Mock(name_, symbol_, decimals_) {} - function transfer(address recipient, uint256 amount) public virtual override(ERC20, IERC20) returns (bool) { + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { uint256 fee = _computeFee(amount); _transfer(_msgSender(), recipient, amount - fee); if (fee > 0) _transfer(_msgSender(), owner(), fee); return true; } - function transferFrom(address sender, address recipient, uint256 amount) - public - virtual - override(ERC20, IERC20) - returns (bool) - { + function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { uint256 fee = _computeFee(amount); if (fee > 0) _transfer(sender, owner(), fee); return super.transferFrom(sender, recipient, amount - fee); diff --git a/contracts/test/suites/CreditFacadeTestSuite.sol b/contracts/test/suites/CreditFacadeTestSuite.sol index d9544da9..27e5a9db 100644 --- a/contracts/test/suites/CreditFacadeTestSuite.sol +++ b/contracts/test/suites/CreditFacadeTestSuite.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {CreditFacadeV3} from "../../credit/CreditFacadeV3.sol"; import {CreditConfigurator} from "../../credit/CreditConfiguratorV3.sol"; @@ -83,9 +83,8 @@ contract CreditFacadeTestSuite is PoolDeployer { cmOpts.degenNFT = address(degenNFT); } - cmOpts.withdrawalManager = address(withdrawalManager); - CreditManagerFactory cmf = new CreditManagerFactory( + address(addressProvider), address(poolMock), cmOpts, 0 @@ -110,7 +109,7 @@ contract CreditFacadeTestSuite is PoolDeployer { if (accountFactoryVer == 2) { vm.prank(CONFIGURATOR); - AccountFactoryV3(address(af)).addCreditManager(address(creditManager), 1); + AccountFactoryV3(address(af)).addCreditManager(address(creditManager)); } if (supportQuotas) { @@ -118,6 +117,9 @@ contract CreditFacadeTestSuite is PoolDeployer { poolQuotaKeeper.addCreditManager(address(creditManager)); } + vm.prank(CONFIGURATOR); + botList.setApprovedCreditManagerStatus(address(creditManager), true); + vm.label(address(poolMock), "Pool"); vm.label(address(creditFacade), "CreditFacadeV3"); vm.label(address(creditManager), "CreditManagerV3"); diff --git a/contracts/test/suites/CreditManagerTestSuite.sol b/contracts/test/suites/CreditManagerTestSuite.sol index 39046d0a..659ebebe 100644 --- a/contracts/test/suites/CreditManagerTestSuite.sol +++ b/contracts/test/suites/CreditManagerTestSuite.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {CreditManagerV3} from "../../credit/CreditManagerV3.sol"; import {CreditManagerOpts, CollateralToken} from "../../credit/CreditConfiguratorV3.sol"; @@ -15,7 +15,7 @@ import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/P import "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; import "../lib/constants.sol"; -import {CreditManagerTestInternal} from "../mocks/credit/CreditManagerTestInternal.sol"; +import {CreditManagerV3Harness} from "../unit/credit/CreditManagerV3Harness.sol"; import {PoolDeployer} from "./PoolDeployer.sol"; import {ICreditConfig} from "../interfaces/ICreditConfig.sol"; import {ITokenTestSuite} from "../interfaces/ITokenTestSuite.sol"; @@ -57,8 +57,8 @@ contract CreditManagerTestSuite is PoolDeployer { tokenTestSuite = creditConfig.tokenTestSuite(); creditManager = internalSuite - ? new CreditManagerTestInternal(address(poolMock), address(withdrawalManager)) - : new CreditManagerV3(address(poolMock), address(withdrawalManager)); + ? new CreditManagerV3Harness(address(addressProvider), address(poolMock)) + : new CreditManagerV3(address(addressProvider), address(poolMock)); creditFacade = msg.sender; @@ -67,7 +67,7 @@ contract CreditManagerTestSuite is PoolDeployer { vm.startPrank(CONFIGURATOR); creditManager.setCreditFacade(creditFacade); - creditManager.setParams( + creditManager.setFees( DEFAULT_FEE_INTEREST, DEFAULT_FEE_LIQUIDATION, PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM, @@ -100,7 +100,7 @@ contract CreditManagerTestSuite is PoolDeployer { } if (accountFactoryVer == 2) { - AccountFactoryV3(address(af)).addCreditManager(address(creditManager), 1); + AccountFactoryV3(address(af)).addCreditManager(address(creditManager)); } vm.stopPrank(); @@ -148,18 +148,18 @@ contract CreditManagerTestSuite is PoolDeployer { borrowedAmount = _borrowedAmount; cumulativeIndexLastUpdate = RAY; - poolMock.setCumulative_RAY(cumulativeIndexLastUpdate); + poolMock.setCumulativeIndexNow(cumulativeIndexLastUpdate); vm.prank(creditFacade); // Existing address case - creditAccount = creditManager.openCreditAccount(borrowedAmount, USER, false); + creditAccount = creditManager.openCreditAccount(borrowedAmount, USER); // Increase block number cause it's forbidden to close credit account in the same block vm.roll(block.number + 1); cumulativeIndexAtClose = (cumulativeIndexLastUpdate * 12) / 10; - poolMock.setCumulative_RAY(cumulativeIndexAtClose); + poolMock.setCumulativeIndexNow(cumulativeIndexAtClose); } function makeTokenQuoted(address token, uint16 rate, uint96 limit) external { @@ -172,7 +172,7 @@ contract CreditManagerTestSuite is PoolDeployer { gaugeMock.updateEpoch(); uint256 tokenMask = creditManager.getTokenMaskOrRevert(token); - uint256 limitedMask = creditManager.quotedTokenMask(); + uint256 limitedMask = creditManager.quotedTokensMask(); creditManager.setQuotedMask(limitedMask | tokenMask); diff --git a/contracts/test/suites/GenesisFactory.sol b/contracts/test/suites/GenesisFactory.sol index b903277b..2fd22323 100644 --- a/contracts/test/suites/GenesisFactory.sol +++ b/contracts/test/suites/GenesisFactory.sol @@ -1,47 +1,48 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox. Generalized leverage protocol that allows to take leverage and then use it across other DeFi protocols and platforms in a composable way. // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {AddressProvider} from "@gearbox-protocol/core-v2/contracts/core/AddressProvider.sol"; +import {AddressProviderV3} from "../../core/AddressProviderV3.sol"; import {ContractsRegister} from "@gearbox-protocol/core-v2/contracts/core/ContractsRegister.sol"; import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; import {DataCompressor} from "@gearbox-protocol/core-v2/contracts/core/DataCompressor.sol"; import {AccountFactory} from "@gearbox-protocol/core-v2/contracts/core/AccountFactory.sol"; import {AccountFactoryV3} from "../../core/AccountFactoryV3.sol"; +import "../../interfaces/IAddressProviderV3.sol"; +import {WithdrawalManager} from "../../support/WithdrawalManager.sol"; +import {BotList} from "../../support/BotList.sol"; import {WETHGateway} from "../../support/WETHGateway.sol"; import {PriceOracle, PriceFeedConfig} from "@gearbox-protocol/core-v2/contracts/oracles/PriceOracle.sol"; import {GearToken} from "@gearbox-protocol/core-v2/contracts/tokens/GearToken.sol"; contract GenesisFactory is Ownable { - AddressProvider public addressProvider; + AddressProviderV3 public addressProvider; ACL public acl; PriceOracle public priceOracle; constructor(address wethToken, address treasury, uint8 accountFactoryVer) { - addressProvider = new AddressProvider(); // T:[GD-1] - addressProvider.setWethToken(wethToken); // T:[GD-1] - addressProvider.setTreasuryContract(treasury); // T:[GD-1] - acl = new ACL(); // T:[GD-1] - addressProvider.setACL(address(acl)); // T:[GD-1] + addressProvider = new AddressProviderV3(address(acl)); // T:[GD-1] + addressProvider.setAddress(AP_WETH_TOKEN, wethToken, false); // T:[GD-1] + addressProvider.setAddress(AP_TREASURY, treasury, false); // T:[GD-1] ContractsRegister contractsRegister = new ContractsRegister( address(addressProvider) ); // T:[GD-1] - addressProvider.setContractsRegister(address(contractsRegister)); // T:[GD-1] + addressProvider.setAddress(AP_CONTRACTS_REGISTER, address(contractsRegister), true); // T:[GD-1] DataCompressor dataCompressor = new DataCompressor( address(addressProvider) ); // T:[GD-1] - addressProvider.setDataCompressor(address(dataCompressor)); // T:[GD-1] + addressProvider.setAddress(AP_DATA_COMPRESSOR, address(dataCompressor), true); // T:[GD-1] PriceFeedConfig[] memory config; priceOracle = new PriceOracle(address(addressProvider), config); // T:[GD-1] - addressProvider.setPriceOracle(address(priceOracle)); // T:[GD-1] + addressProvider.setAddress(AP_PRICE_ORACLE, address(priceOracle), true); // T:[GD-1] address accountFactory; @@ -57,15 +58,20 @@ contract GenesisFactory is Ownable { accountFactory = address(new AccountFactoryV3( address(addressProvider))); // T:[GD-1] } - addressProvider.setAccountFactory(accountFactory); // T:[GD-1] + addressProvider.setAddress(AP_ACCOUNT_FACTORY, accountFactory, false); // T:[GD-1] WETHGateway wethGateway = new WETHGateway(address(addressProvider)); // T:[GD-1] - addressProvider.setWETHGateway(address(wethGateway)); // T:[GD-1] + addressProvider.setAddress(AP_WETH_GATEWAY, address(wethGateway), true); // T:[GD-1] + + WithdrawalManager wm = new WithdrawalManager(address(addressProvider), 1 days); + addressProvider.setAddress(AP_WITHDRAWAL_MANAGER, address(wm), true); + + BotList botList = new BotList(address(addressProvider)); + addressProvider.setAddress(AP_BOT_LIST, address(botList), true); GearToken gearToken = new GearToken(address(this)); // T:[GD-1] - addressProvider.setGearToken(address(gearToken)); // T:[GD-1] + addressProvider.setAddress(AP_GEAR_TOKEN, address(gearToken), false); // T:[GD-1] gearToken.transferOwnership(msg.sender); // T:[GD-1] - addressProvider.transferOwnership(msg.sender); // T:[GD-1] acl.transferOwnership(msg.sender); // T:[GD-1] } @@ -83,8 +89,4 @@ contract GenesisFactory is Ownable { function claimACLOwnership() external onlyOwner { acl.claimOwnership(); } - - function claimAddressProviderOwnership() external onlyOwner { - addressProvider.claimOwnership(); - } } diff --git a/contracts/test/suites/PoolDeployer.sol b/contracts/test/suites/PoolDeployer.sol index e897d5eb..e4246e1f 100644 --- a/contracts/test/suites/PoolDeployer.sol +++ b/contracts/test/suites/PoolDeployer.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; -import {AddressProvider} from "@gearbox-protocol/core-v2/contracts/core/AddressProvider.sol"; +import "../../interfaces/IAddressProviderV3.sol"; import {IPriceOracleV2Ext} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; import {PriceFeedConfig} from "@gearbox-protocol/core-v2/contracts/oracles/PriceOracle.sol"; import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; @@ -12,10 +12,11 @@ import {AccountFactory} from "@gearbox-protocol/core-v2/contracts/core/AccountFa import {GenesisFactory} from "./GenesisFactory.sol"; import {PoolFactory, PoolOpts} from "@gearbox-protocol/core-v2/contracts/factories/PoolFactory.sol"; import {WithdrawalManager} from "../../support/WithdrawalManager.sol"; +import {BotList} from "../../support/BotList.sol"; import {CreditManagerOpts, CollateralToken} from "../../credit/CreditConfiguratorV3.sol"; -import {PoolServiceMock} from "../mocks/pool/PoolServiceMock.sol"; -import {GaugeMock} from "../mocks/pool/GaugeMock.sol"; +import {PoolMock} from "../mocks//pool/PoolMock.sol"; +import {GaugeMock} from "../mocks//pool/GaugeMock.sol"; import {PoolQuotaKeeper} from "../../pool/PoolQuotaKeeper.sol"; import "../lib/constants.sol"; @@ -31,14 +32,15 @@ struct PoolCreditOpts { /// @title CreditManagerTestSuite /// @notice Deploys contract for unit testing of CreditManagerV3.sol contract PoolDeployer is Test { - AddressProvider public addressProvider; + IAddressProviderV3 public addressProvider; GenesisFactory public gp; AccountFactory public af; - PoolServiceMock public poolMock; + PoolMock public poolMock; PoolQuotaKeeper public poolQuotaKeeper; GaugeMock public gaugeMock; ContractsRegister public cr; WithdrawalManager public withdrawalManager; + BotList public botList; ACL public acl; IPriceOracleV2Ext public priceOracle; @@ -58,7 +60,6 @@ contract PoolDeployer is Test { gp = new GenesisFactory(wethToken, DUMB_ADDRESS, accountFactoryVersion); gp.acl().claimOwnership(); - gp.addressProvider().claimOwnership(); gp.acl().addPausableAdmin(CONFIGURATOR); gp.acl().addUnpausableAdmin(CONFIGURATOR); @@ -70,19 +71,21 @@ contract PoolDeployer is Test { gp.acl().claimOwnership(); addressProvider = gp.addressProvider(); - af = AccountFactory(addressProvider.getAccountFactory()); + af = AccountFactory(addressProvider.getAddressOrRevert(AP_ACCOUNT_FACTORY, NO_VERSION_CONTROL)); - priceOracle = IPriceOracleV2Ext(addressProvider.getPriceOracle()); + priceOracle = IPriceOracleV2Ext(addressProvider.getAddressOrRevert(AP_PRICE_ORACLE, 2)); - acl = ACL(addressProvider.getACL()); + acl = ACL(addressProvider.getAddressOrRevert(AP_ACL, 0)); - cr = ContractsRegister(addressProvider.getContractsRegister()); + cr = ContractsRegister(addressProvider.getAddressOrRevert(AP_CONTRACTS_REGISTER, 1)); - withdrawalManager = new WithdrawalManager(address(addressProvider), 1 days); + withdrawalManager = WithdrawalManager(addressProvider.getAddressOrRevert(AP_WITHDRAWAL_MANAGER, 3_00)); + + botList = BotList(addressProvider.getAddressOrRevert(AP_BOT_LIST, 3_00)); underlying = _underlying; - poolMock = new PoolServiceMock( + poolMock = new PoolMock( address(gp.addressProvider()), underlying ); @@ -101,13 +104,11 @@ contract PoolDeployer is Test { poolMock.setPoolQuotaKeeper(address(poolQuotaKeeper)); - addressProvider.transferOwnership(CONFIGURATOR); acl.transferOwnership(CONFIGURATOR); vm.startPrank(CONFIGURATOR); acl.claimOwnership(); - addressProvider.claimOwnership(); vm.stopPrank(); } diff --git a/contracts/test/suites/PoolQuotaKeeperTestSuite.sol b/contracts/test/suites/PoolQuotaKeeperTestSuite.sol deleted file mode 100644 index 29a62915..00000000 --- a/contracts/test/suites/PoolQuotaKeeperTestSuite.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; - -import {AddressProvider} from "@gearbox-protocol/core-v2/contracts/core/AddressProvider.sol"; -import {ContractsRegister} from "@gearbox-protocol/core-v2/contracts/core/ContractsRegister.sol"; -import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; -import {DieselToken} from "@gearbox-protocol/core-v2/contracts/tokens/DieselToken.sol"; - -import {IPool4626} from "../../interfaces/IPool4626.sol"; -import {TestPoolService} from "@gearbox-protocol/core-v2/contracts/test/mocks/pool/TestPoolService.sol"; -import {Tokens} from "../config/Tokens.sol"; - -import {LinearInterestRateModel} from "../../pool/LinearInterestRateModel.sol"; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {CreditManagerMockForPoolTest} from "../mocks/pool/CreditManagerMockForPoolTest.sol"; -import {WETHMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/WETHMock.sol"; -import {ERC20FeeMock} from "../mocks/token/ERC20FeeMock.sol"; - -import "../lib/constants.sol"; -import {ITokenTestSuite} from "../interfaces/ITokenTestSuite.sol"; -import {Pool4626} from "../../pool/Pool4626.sol"; -import {PoolQuotaKeeper} from "../../pool/PoolQuotaKeeper.sol"; -import {GaugeMock} from "../mocks/pool/GaugeMock.sol"; - -import {PoolServiceMock} from "../mocks/pool/PoolServiceMock.sol"; -import {Test} from "forge-std/Test.sol"; - -uint256 constant liquidityProviderInitBalance = 100 ether; -uint256 constant addLiquidity = 10 ether; -uint256 constant removeLiquidity = 5 ether; -uint16 constant referral = 12333; - -/// @title PoolServiceTestSuite -/// @notice Deploys contract for unit testing of PoolService.sol -contract PoolQuotaKeeperTestSuite is Test { - ACL public acl; - WETHMock public weth; - - AddressProvider public addressProvider; - ContractsRegister public cr; - - PoolServiceMock public pool4626; - CreditManagerMockForPoolTest public cmMock; - IERC20 public underlying; - - PoolQuotaKeeper public poolQuotaKeeper; - GaugeMock public gaugeMock; - - address public treasury; - - constructor(ITokenTestSuite _tokenTestSuite, address _underlying) { - vm.startPrank(CONFIGURATOR); - - acl = new ACL(); - weth = WETHMock(payable(_tokenTestSuite.wethToken())); - addressProvider = new AddressProvider(); - addressProvider.setACL(address(acl)); - addressProvider.setTreasuryContract(DUMB_ADDRESS2); - cr = new ContractsRegister(address(addressProvider)); - addressProvider.setContractsRegister(address(cr)); - treasury = DUMB_ADDRESS2; - addressProvider.setWethToken(address(weth)); - - underlying = IERC20(_underlying); - - _tokenTestSuite.mint(_underlying, USER, liquidityProviderInitBalance); - _tokenTestSuite.mint(_underlying, INITIAL_LP, liquidityProviderInitBalance); - - pool4626 = new PoolServiceMock(address(addressProvider), _underlying); - - poolQuotaKeeper = new PoolQuotaKeeper(address(pool4626)); - - // vm.prank(CONFIGURATOR); - pool4626.connectPoolQuotaManager(address(poolQuotaKeeper)); - - gaugeMock = new GaugeMock(address(pool4626)); - - // vm.prank(CONFIGURATOR); - poolQuotaKeeper.setGauge(address(gaugeMock)); - - vm.stopPrank(); - - vm.startPrank(CONFIGURATOR); - - cmMock = new CreditManagerMockForPoolTest(address(pool4626)); - - cr.addPool(address(pool4626)); - cr.addCreditManager(address(cmMock)); - - vm.label(address(pool4626), "Pool"); - - vm.stopPrank(); - } -} diff --git a/contracts/test/suites/PoolServiceTestSuite.sol b/contracts/test/suites/PoolServiceTestSuite.sol deleted file mode 100644 index 660040a8..00000000 --- a/contracts/test/suites/PoolServiceTestSuite.sol +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; - -import {AddressProvider} from "@gearbox-protocol/core-v2/contracts/core/AddressProvider.sol"; -import {ContractsRegister} from "@gearbox-protocol/core-v2/contracts/core/ContractsRegister.sol"; -import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; -import {DieselToken} from "@gearbox-protocol/core-v2/contracts/tokens/DieselToken.sol"; - -import {IPool4626} from "../../interfaces/IPool4626.sol"; -import {TestPoolService} from "@gearbox-protocol/core-v2/contracts/test/mocks/pool/TestPoolService.sol"; -import {Tokens} from "../config/Tokens.sol"; - -import {LinearInterestRateModel} from "../../pool/LinearInterestRateModel.sol"; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {CreditManagerMockForPoolTest} from "../mocks/pool/CreditManagerMockForPoolTest.sol"; -import {WETHMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/WETHMock.sol"; -import {ERC20FeeMock} from "../mocks/token/ERC20FeeMock.sol"; - -import "../lib/constants.sol"; -import {ITokenTestSuite} from "../interfaces/ITokenTestSuite.sol"; -import {Pool4626} from "../../pool/Pool4626.sol"; -import {PoolQuotaKeeper} from "../../pool/PoolQuotaKeeper.sol"; -import {GaugeMock} from "../mocks/pool/GaugeMock.sol"; - -import {Pool4626_USDT} from "../../pool/Pool4626_USDT.sol"; - -import {Test} from "forge-std/Test.sol"; - -uint256 constant liquidityProviderInitBalance = 100 ether; -uint256 constant addLiquidity = 10 ether; -uint256 constant removeLiquidity = 5 ether; -uint16 constant referral = 12333; - -/// @title PoolServiceTestSuite -/// @notice Deploys contract for unit testing of PoolService.sol -contract PoolServiceTestSuite is Test { - ACL public acl; - WETHMock public weth; - - AddressProvider public addressProvider; - ContractsRegister public cr; - TestPoolService public poolService; - Pool4626 public pool4626; - CreditManagerMockForPoolTest public cmMock; - IERC20 public underlying; - DieselToken public dieselToken; - LinearInterestRateModel public linearIRModel; - PoolQuotaKeeper public poolQuotaKeeper; - GaugeMock public gaugeMock; - - address public treasury; - - constructor(ITokenTestSuite _tokenTestSuite, address _underlying, bool is4626, bool supportQuotas) { - linearIRModel = new LinearInterestRateModel( - 80_00, - 90_00, - 2_00, - 4_00, - 40_00, - 75_00, - false - ); - - vm.startPrank(CONFIGURATOR); - - acl = new ACL(); - weth = WETHMock(payable(_tokenTestSuite.wethToken())); - addressProvider = new AddressProvider(); - addressProvider.setACL(address(acl)); - addressProvider.setTreasuryContract(DUMB_ADDRESS2); - cr = new ContractsRegister(address(addressProvider)); - addressProvider.setContractsRegister(address(cr)); - treasury = DUMB_ADDRESS2; - addressProvider.setWethToken(address(weth)); - - underlying = IERC20(_underlying); - - _tokenTestSuite.mint(_underlying, USER, liquidityProviderInitBalance); - _tokenTestSuite.mint(_underlying, INITIAL_LP, liquidityProviderInitBalance); - - address newPool; - - bool isFeeToken = false; - - try ERC20FeeMock(_underlying).basisPointsRate() returns (uint256) { - isFeeToken = true; - } catch {} - - if (is4626) { - pool4626 = isFeeToken - ? new Pool4626_USDT({ - _addressProvider: address(addressProvider), - _underlyingToken: _underlying, - _interestRateModel: address(linearIRModel), - _expectedLiquidityLimit: type(uint256).max, - _supportsQuotas: supportQuotas - }) - : new Pool4626({ - _addressProvider: address(addressProvider), - _underlyingToken: _underlying, - _interestRateModel: address(linearIRModel), - _expectedLiquidityLimit: type(uint256).max, - _supportsQuotas: supportQuotas - }); - newPool = address(pool4626); - - if (supportQuotas) { - _deployAndConnectPoolQuotaKeeper(); - } - } else { - poolService = new TestPoolService( - address(addressProvider), - address(underlying), - address(linearIRModel), - type(uint256).max - ); - newPool = address(poolService); - dieselToken = DieselToken(poolService.dieselToken()); - vm.label(address(dieselToken), "DieselToken"); - } - - vm.stopPrank(); - - vm.prank(USER); - underlying.approve(newPool, type(uint256).max); - - vm.prank(INITIAL_LP); - underlying.approve(newPool, type(uint256).max); - - vm.startPrank(CONFIGURATOR); - - cmMock = new CreditManagerMockForPoolTest(newPool); - - cr.addPool(newPool); - cr.addCreditManager(address(cmMock)); - - vm.label(newPool, "Pool"); - - // vm.label(address(underlying), "UnderlyingToken"); - - vm.stopPrank(); - } - - function _deployAndConnectPoolQuotaKeeper() internal { - poolQuotaKeeper = new PoolQuotaKeeper(address(pool4626)); - - // vm.prank(CONFIGURATOR); - pool4626.connectPoolQuotaManager(address(poolQuotaKeeper)); - - gaugeMock = new GaugeMock(address(pool4626)); - - // vm.prank(CONFIGURATOR); - poolQuotaKeeper.setGauge(address(gaugeMock)); - } -} diff --git a/contracts/test/suites/TokensTestSuite.sol b/contracts/test/suites/TokensTestSuite.sol index 94cc553d..5ac7a671 100644 --- a/contracts/test/suites/TokensTestSuite.sol +++ b/contracts/test/suites/TokensTestSuite.sol @@ -1,19 +1,19 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox. Generalized leverage protocol that allows to take leverage and then use it across other DeFi protocols and platforms in a composable way. // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {WETHMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/WETHMock.sol"; -import {ERC20BlacklistableMock} from "../mocks/token/ERC20Blacklistable.sol"; +import {ERC20BlacklistableMock} from "../mocks//token/ERC20Blacklistable.sol"; import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import {PriceFeedConfig} from "@gearbox-protocol/core-v2/contracts/oracles/PriceOracle.sol"; // MOCKS import {ERC20Mock} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/ERC20Mock.sol"; -import {ERC20FeeMock} from "../mocks/token/ERC20FeeMock.sol"; +import {ERC20FeeMock} from "../mocks//token/ERC20FeeMock.sol"; import {PriceFeedMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/oracles/PriceFeedMock.sol"; @@ -92,19 +92,45 @@ contract TokensTestSuite is Test, TokensData, TokensTestSuiteHelper { return balanceOf(addressOf[t], holder); } - function approve(Tokens t, address holder, address targetContract) public { - approve(addressOf[t], holder, targetContract); + function approve(Tokens t, address from, address spender) public { + approve(addressOf[t], from, spender); } - function approve(Tokens t, address holder, address targetContract, uint256 amount) public { - approve(addressOf[t], holder, targetContract, amount); + function approve(Tokens t, address from, address spender, uint256 amount) public { + approve(addressOf[t], from, spender, amount); } - function allowance(Tokens t, address holder, address targetContract) external view returns (uint256) { - return IERC20(addressOf[t]).allowance(holder, targetContract); + function allowance(Tokens t, address from, address spender) external view returns (uint256) { + return IERC20(addressOf[t]).allowance(from, spender); } function burn(Tokens t, address from, uint256 amount) external { burn(addressOf[t], from, amount); } + + function listOf(Tokens t1) external view returns (address[] memory tokensList) { + tokensList = new address[](1); + tokensList[0] = addressOf[t1]; + } + + function listOf(Tokens t1, Tokens t2) external view returns (address[] memory tokensList) { + tokensList = new address[](2); + tokensList[0] = addressOf[t1]; + tokensList[1] = addressOf[t2]; + } + + function listOf(Tokens t1, Tokens t2, Tokens t3) external view returns (address[] memory tokensList) { + tokensList = new address[](3); + tokensList[0] = addressOf[t1]; + tokensList[1] = addressOf[t2]; + tokensList[2] = addressOf[t3]; + } + + function listOf(Tokens t1, Tokens t2, Tokens t3, Tokens t4) external view returns (address[] memory tokensList) { + tokensList = new address[](4); + tokensList[0] = addressOf[t1]; + tokensList[1] = addressOf[t2]; + tokensList[2] = addressOf[t3]; + tokensList[3] = addressOf[t4]; + } } diff --git a/contracts/test/suites/TokensTestSuiteHelper.sol b/contracts/test/suites/TokensTestSuiteHelper.sol index b904ed35..423b14db 100644 --- a/contracts/test/suites/TokensTestSuiteHelper.sol +++ b/contracts/test/suites/TokensTestSuiteHelper.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox. Generalized leverage protocol that allows to take leverage and then use it across other DeFi protocols and platforms in a composable way. // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/test/unit/adapters/AbstractAdapter.t.sol b/contracts/test/unit/adapters/AbstractAdapter.t.sol index b7ff795f..5827b968 100644 --- a/contracts/test/unit/adapters/AbstractAdapter.t.sol +++ b/contracts/test/unit/adapters/AbstractAdapter.t.sol @@ -3,222 +3,165 @@ // (c) Gearbox Holdings, 2023 pragma solidity ^0.8.17; -import { - CallerNotCreditFacadeException, - ExternalCallCreditAccountNotSetException, - TokenNotAllowedException, - ZeroAddressException -} from "../../../interfaces/IExceptions.sol"; -import {IPool4626} from "../../../interfaces/IPool4626.sol"; +import {CallerNotCreditFacadeException, ZeroAddressException} from "../../../interfaces/IExceptions.sol"; -import {CreditConfig} from "../../config/CreditConfig.sol"; -import {Tokens} from "../../config/Tokens.sol"; +import {TestHelper} from "../../lib/helper.sol"; +import {AddressProviderV3ACLMock} from "../../mocks/core/AddressProviderV3ACLMock.sol"; -import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; -import {CreditFacadeTestHelper} from "../../helpers/CreditFacadeTestHelper.sol"; +import {AbstractAdapterHarness} from "./AbstractAdapterHarness.sol"; +import {CreditManagerMock, CreditManagerMockEvents} from "./CreditManagerMock.sol"; -import {CONFIGURATOR, USER} from "../../lib/constants.sol"; +/// @title Abstract adapter unit test +/// @notice U:[AA]: `AbstractAdapter` unit tests +contract AbstractAdapterUnitTest is TestHelper, CreditManagerMockEvents { + AbstractAdapterHarness abstractAdapter; + AddressProviderV3ACLMock addressProvider; + CreditManagerMock creditManager; -import {AdapterMock} from "../../mocks/adapters/AdapterMock.sol"; -import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/adapters/TargetContractMock.sol"; - -import {CreditFacadeTestSuite} from "../../suites/CreditFacadeTestSuite.sol"; -import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; - -import {Test} from "forge-std/Test.sol"; - -/// @title AbstractAdapterTest -/// @notice Designed for unit test purposes only -contract AbstractAdapterTest is Test, BalanceHelper, CreditFacadeTestHelper { - TargetContractMock targetMock; - AdapterMock adapterMock; - - address usdc; - address dai; + address facade; + address target; function setUp() public { - tokenTestSuite = new TokensTestSuite(); - - CreditConfig creditConfig = new CreditConfig( - tokenTestSuite, - Tokens.DAI - ); - - cft = new CreditFacadeTestSuite(creditConfig, false, false, false, 1); - - underlying = tokenTestSuite.addressOf(Tokens.DAI); - creditManager = cft.creditManager(); - creditFacade = cft.creditFacade(); - creditConfigurator = cft.creditConfigurator(); + facade = makeAddr("CREDIT_FACADE"); + target = makeAddr("TARGET_CONTRACT"); - targetMock = new TargetContractMock(); - adapterMock = new AdapterMock( - address(creditManager), - address(targetMock) - ); - - vm.prank(CONFIGURATOR); - creditConfigurator.allowContract(address(targetMock), address(adapterMock)); - - vm.label(address(adapterMock), "AdapterMock"); - vm.label(address(targetMock), "TargetContractMock"); - - usdc = tokenTestSuite.addressOf(Tokens.USDC); - dai = tokenTestSuite.addressOf(Tokens.DAI); + addressProvider = new AddressProviderV3ACLMock(); + creditManager = new CreditManagerMock(address(addressProvider), facade); + abstractAdapter = new AbstractAdapterHarness(address(creditManager), target); } - /// ----- /// - /// TESTS /// - /// ----- /// - - /// @notice [AA-1]: Constructor reverts when passed zero-address as credit manager or target contract - function test_AA_01_constructor_reverts_on_zero_address() public { + /// @notice U:[AA-1A]: constructor reverts on zero address + function test_U_AA_01A_constructor_reverts_on_zero_address() public { vm.expectRevert(); - new AdapterMock(address(0), address(0)); + new AbstractAdapterHarness(address(0), address(0)); vm.expectRevert(ZeroAddressException.selector); - new AdapterMock(address(creditManager), address(0)); + new AbstractAdapterHarness(address(creditManager), address(0)); } - /// @notice [AA-2]: Constructor sets correct values - function test_AA_02_constructor_sets_correct_values() public { - assertEq(address(adapterMock.creditManager()), address(creditManager), "Incorrect credit manager"); - - assertEq( - address(adapterMock.addressProvider()), - address(IPool4626(creditManager.pool()).addressProvider()), - "Incorrect address provider" - ); - - assertEq(adapterMock.targetContract(), address(targetMock), "Incorrect target contract"); + /// @notice U:[AA-1B]: constructor sets correct values + function test_U_AA_01B_constructor_sets_correct_values() public { + assertEq(abstractAdapter.creditManager(), address(creditManager), "Incorrect credit manager"); + assertEq(abstractAdapter.addressProvider(), address(addressProvider), "Incorrect address provider"); + assertEq(abstractAdapter.targetContract(), target, "Incorrect target contract"); } - /// @notice [AA-3]: `creditFacadeOnly` functions revert if called not by the credit facade - function test_AA_03_creditFacadeOnly_function_reverts_if_called_not_by_credit_facade() public { + /// @notice U:[AA-2]: `_revertIfCallerNotCreditFacade` works correctly + function test_U_AA_02_revertIfCallerNotCreditFacade_works_correctly(address caller) public { + vm.assume(caller != facade); + vm.expectRevert(CallerNotCreditFacadeException.selector); - vm.prank(USER); - adapterMock.dumbCall(0, 0); + vm.prank(caller); + abstractAdapter.revertIfCallerNotCreditFacade(); } - /// @notice [AA-4]: AbstractAdapter uses correct credit account - function test_AA_04_adapter_uses_correct_credit_account() public { - vm.expectRevert(ExternalCallCreditAccountNotSetException.selector); - vm.prank(address(creditFacade)); - adapterMock.creditAccount(); + /// @notice U:[AA-3]: `_creditAccount` works correctly + function test_U_AA_03_creditAccount_works_correctly(address creditAccount) public { + creditManager.setActiveCreditAccount(creditAccount); - address creditAccount = _openExternalCallCreditAccount(); - assertEq(adapterMock.creditAccount(), creditAccount); + vm.expectCall(address(creditManager), abi.encodeCall(creditManager.getActiveCreditAccountOrRevert, ())); + assertEq(abstractAdapter.creditAccount(), creditAccount, "Incorrect external call credit account"); } - /// @notice [AA-5]: `_getMaskOrRevert` works correctly - function test_AA_05_getMaskOrRevert_works_correctly() public { - vm.expectRevert(TokenNotAllowedException.selector); - adapterMock.getMaskOrRevert(address(0xdead)); + /// @notice U:[AA-4]: `_getMaskOrRevert` works correctly + function test_U_AA_04_getMaskOrRevert_works_correctly(address token, uint8 index) public { + uint256 mask = 1 << index; + creditManager.setMask(token, mask); - assertEq( - adapterMock.getMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)), - creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)) - ); + vm.expectCall(address(creditManager), abi.encodeCall(creditManager.getTokenMaskOrRevert, (token))); + assertEq(abstractAdapter.getMaskOrRevert(token), mask, "Incorrect token mask"); } - /// @notice [AA-6]: `_approveToken` correctly passes parameters to the credit manager - function test_AA_06_approveToken_correctly_passes_to_credit_manager() public { - _openExternalCallCreditAccount(); - - vm.expectCall(address(creditManager), abi.encodeCall(creditManager.approveCreditAccount, (usdc, 10))); - vm.prank(USER); - adapterMock.approveToken(usdc, 10); + /// @notice U:[AA-5]: `_approveToken` works correctly + function test_U_AA_05_approveToken_works_correctly(address token, uint256 amount) public { + vm.expectCall(address(creditManager), abi.encodeCall(creditManager.approveCreditAccount, (token, amount))); + abstractAdapter.approveToken(token, amount); } - /// @notice [AA-7]: `_execute` correctly passes parameters to the credit manager - function test_AA_07_execute_correctly_passes_to_credit_manager() public { - _openExternalCallCreditAccount(); - - bytes memory callData = adapterMock.dumbCallData(); + /// @notice U:[AA-6]: `_execute` works correctly + function test_U_AA_06_execute_works_correctly(bytes memory data, bytes memory expectedResult) public { + creditManager.setExecuteOrderResult(expectedResult); - vm.expectCall(address(creditManager), abi.encodeCall(creditManager.executeOrder, (callData))); - vm.prank(USER); - adapterMock.execute(callData); + vm.expectCall(address(creditManager), abi.encodeCall(creditManager.executeOrder, (data))); + assertEq(abstractAdapter.execute(data), expectedResult, "Incorrect result"); } - /// @notice [AA-8]: `_executeSwapNoApprove` works correctly - function test_AA_08_executeSwapNoApprove_works_correctly() public { - address creditAccount = _openExternalCallCreditAccount(); - - bytes memory callData = adapterMock.dumbCallData(); - for (uint256 dt = 0; dt < 2; ++dt) { - bool disableTokenIn = dt == 1; - - vm.expectCall(address(creditManager), abi.encodeCall(creditManager.executeOrder, (callData))); - - vm.prank(USER); - (uint256 tokensToEnable, uint256 tokensToDisable,) = - adapterMock.executeSwapNoApprove(usdc, dai, callData, disableTokenIn); + /// @notice U:[AA-7]: `_executeSwapNoApprove` works correctly + function test_U_AA_07_executeSwapNoApprove_works_correctly( + address tokenIn, + address tokenOut, + uint8 tokenInIndex, + uint8 tokenOutIndex, + bytes memory data, + bytes memory expectedResult + ) public { + if (tokenIn == tokenOut) tokenOutIndex = tokenInIndex; + creditManager.setMask(tokenIn, 1 << tokenInIndex); + creditManager.setMask(tokenOut, 1 << tokenOutIndex); + creditManager.setExecuteOrderResult(expectedResult); + + uint256 snapshot = vm.snapshot(); + for (uint256 caseNumber; caseNumber < 2; ++caseNumber) { + bool disableTokenIn = caseNumber == 1; + string memory caseName = caseNumber == 1 ? "disableTokenIn = true" : "disableTokenIn = false"; + + vm.expectEmit(false, false, false, false); + emit Execute(); + + (uint256 tokensToEnable, uint256 tokensToDisable, bytes memory result) = + abstractAdapter.executeSwapNoApprove(tokenIn, tokenOut, data, disableTokenIn); + + assertEq(tokensToEnable, 1 << tokenOutIndex, _testCaseErr(caseName, "Incorrect tokensToEnable")); + assertEq( + tokensToDisable, + disableTokenIn ? 1 << tokenInIndex : 0, + _testCaseErr(caseName, "Incorrect tokensToDisable") + ); + assertEq(result, expectedResult, _testCaseErr(caseName, "Incorrect result")); - expectAllowance(usdc, creditAccount, address(targetMock), 0); - assertEq(tokensToEnable, creditManager.getTokenMaskOrRevert(dai), "Incorrect tokensToEnable"); - if (disableTokenIn) { - assertEq(tokensToDisable, creditManager.getTokenMaskOrRevert(usdc), "Incorrect tokensToDisable"); - } + vm.revertTo(snapshot); } } - /// @notice [AA-9]: `_executeSwapSafeApprove` works correctly - function test_AA_09_executeSwapSafeApprove_works_correctly() public { - address creditAccount = _openExternalCallCreditAccount(); - - bytes memory callData = adapterMock.dumbCallData(); - for (uint256 dt = 0; dt < 2; ++dt) { - bool disableTokenIn = dt == 1; - - vm.expectCall( - address(creditManager), abi.encodeCall(creditManager.approveCreditAccount, (usdc, type(uint256).max)) + /// @notice U:[AA-8]: `_executeSwapSafeApprove` works correctly + function test_U_AA_08_executeSwapSafeApprove_works_correctly( + address tokenIn, + address tokenOut, + uint8 tokenInIndex, + uint8 tokenOutIndex, + bytes memory data, + bytes memory expectedResult + ) public { + if (tokenIn == tokenOut) tokenOutIndex = tokenInIndex; + creditManager.setMask(tokenIn, 1 << tokenInIndex); + creditManager.setMask(tokenOut, 1 << tokenOutIndex); + creditManager.setExecuteOrderResult(expectedResult); + + uint256 snapshot = vm.snapshot(); + for (uint256 caseNumber; caseNumber < 2; ++caseNumber) { + bool disableTokenIn = caseNumber == 1; + string memory caseName = caseNumber == 1 ? "disableTokenIn = true" : "disableTokenIn = false"; + + // need to ensure that order is correct + vm.expectEmit(true, false, false, true); + emit Approve(tokenIn, type(uint256).max); + vm.expectEmit(false, false, false, false); + emit Execute(); + vm.expectEmit(true, false, false, true); + emit Approve(tokenIn, 1); + + (uint256 tokensToEnable, uint256 tokensToDisable, bytes memory result) = + abstractAdapter.executeSwapSafeApprove(tokenIn, tokenOut, data, disableTokenIn); + + assertEq(tokensToEnable, 1 << tokenOutIndex, _testCaseErr(caseName, "Incorrect tokensToEnable")); + assertEq( + tokensToDisable, + disableTokenIn ? 1 << tokenInIndex : 0, + _testCaseErr(caseName, "Incorrect tokensToDisable") ); - vm.expectCall(address(creditManager), abi.encodeCall(creditManager.executeOrder, (callData))); - vm.expectCall(address(creditManager), abi.encodeCall(creditManager.approveCreditAccount, (usdc, 1))); - - vm.prank(USER); - (uint256 tokensToEnable, uint256 tokensToDisable,) = - adapterMock.executeSwapSafeApprove(usdc, dai, callData, disableTokenIn); - - expectAllowance(usdc, creditAccount, address(targetMock), 1); - assertEq(tokensToEnable, creditManager.getTokenMaskOrRevert(dai), "Incorrect tokensToEnable"); - if (disableTokenIn) { - assertEq(tokensToDisable, creditManager.getTokenMaskOrRevert(usdc), "Incorrect tokensToDisable"); - } - } - } + assertEq(result, expectedResult, _testCaseErr(caseName, "Incorrect result")); - /// @notice [AA-10]: `_executeSwap{No|Safe}Approve` reverts if `tokenIn` or `tokenOut` are not collateral tokens - function test_AA_10_executeSwap_reverts_if_tokenIn_or_tokenOut_are_not_collateral_tokens() public { - _openExternalCallCreditAccount(); - - address token = address(0xdead); - bytes memory callData = adapterMock.dumbCallData(); - for (uint256 ti; ti < 2; ++ti) { - (address tokenIn, address tokenOut) = (ti == 1 ? token : dai, ti == 1 ? dai : token); - for (uint256 dt; dt < 2; ++dt) { - bool disableTokenIn = dt == 1; - for (uint256 sa; sa < 2; ++sa) { - vm.expectRevert(TokenNotAllowedException.selector); - vm.prank(USER); - if (sa == 1) { - adapterMock.executeSwapSafeApprove(tokenIn, tokenOut, callData, disableTokenIn); - } else { - adapterMock.executeSwapNoApprove(tokenIn, tokenOut, callData, disableTokenIn); - } - } - } + vm.revertTo(snapshot); } } - - /// ------- /// - /// HELPERS /// - /// ------- /// - - function _openExternalCallCreditAccount() internal returns (address creditAccount) { - (creditAccount,) = _openTestCreditAccount(); - vm.prank(address(creditFacade)); - creditManager.setCreditAccountForExternalCall(creditAccount); - } } diff --git a/contracts/test/unit/adapters/AbstractAdapterHarness.sol b/contracts/test/unit/adapters/AbstractAdapterHarness.sol new file mode 100644 index 00000000..433a87c6 --- /dev/null +++ b/contracts/test/unit/adapters/AbstractAdapterHarness.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2023 +pragma solidity ^0.8.17; + +import {AbstractAdapter} from "../../../adapters/AbstractAdapter.sol"; +import {AdapterType} from "../../../interfaces/IAdapter.sol"; + +contract AbstractAdapterHarness is AbstractAdapter { + AdapterType public constant override _gearboxAdapterType = AdapterType.ABSTRACT; + uint16 public constant override _gearboxAdapterVersion = 1; + + constructor(address _creditManager, address _targetContract) AbstractAdapter(_creditManager, _targetContract) {} + + function revertIfCallerNotCreditFacade() external view { + _revertIfCallerNotCreditFacade(); + } + + function creditAccount() external view returns (address) { + return _creditAccount(); + } + + function getMaskOrRevert(address token) external view returns (uint256 tokenMask) { + return _getMaskOrRevert(token); + } + + function approveToken(address token, uint256 amount) external { + _approveToken(token, amount); + } + + function execute(bytes memory callData) external returns (bytes memory result) { + result = _execute(callData); + } + + function executeSwapNoApprove(address tokenIn, address tokenOut, bytes memory callData, bool disableTokenIn) + external + returns (uint256 tokensToEnable, uint256 tokensToDisable, bytes memory result) + { + return _executeSwapNoApprove(tokenIn, tokenOut, callData, disableTokenIn); + } + + function executeSwapSafeApprove(address tokenIn, address tokenOut, bytes memory callData, bool disableTokenIn) + external + returns (uint256 tokensToEnable, uint256 tokensToDisable, bytes memory result) + { + return _executeSwapSafeApprove(tokenIn, tokenOut, callData, disableTokenIn); + } +} diff --git a/contracts/test/unit/adapters/CreditManagerMock.sol b/contracts/test/unit/adapters/CreditManagerMock.sol new file mode 100644 index 00000000..57ca3c51 --- /dev/null +++ b/contracts/test/unit/adapters/CreditManagerMock.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2023 +pragma solidity ^0.8.17; + +interface CreditManagerMockEvents { + event Approve(address token, uint256 amount); + event Execute(); +} + +contract CreditManagerMock is CreditManagerMockEvents { + address public addressProvider; + address public creditFacade; + + address public getActiveCreditAccountOrRevert; + mapping(address => uint256) public getTokenMaskOrRevert; + + bytes _result; + + constructor(address _addressProvider, address _creditFacade) { + addressProvider = _addressProvider; + creditFacade = _creditFacade; + } + + function approveCreditAccount(address token, uint256 amount) external { + emit Approve(token, amount); + } + + function executeOrder(bytes memory) external returns (bytes memory result) { + emit Execute(); + return _result; + } + + function setActiveCreditAccount(address creditAccount) external { + getActiveCreditAccountOrRevert = creditAccount; + } + + function setMask(address token, uint256 mask) external { + getTokenMaskOrRevert[token] = mask; + } + + function setExecuteOrderResult(bytes memory result) external { + _result = result; + } +} diff --git a/contracts/test/unit/core/AccountFactoryV3.t.sol b/contracts/test/unit/core/AccountFactoryV3.t.sol new file mode 100644 index 00000000..9de2b602 --- /dev/null +++ b/contracts/test/unit/core/AccountFactoryV3.t.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2023 +pragma solidity ^0.8.17; + +import {CreditAccountV3} from "../../../credit/CreditAccountV3.sol"; +import {CreditAccountInfo, CreditManagerV3} from "../../../credit/CreditManagerV3.sol"; +import {IAccountFactoryV3Events} from "../../../interfaces/IAccountFactoryV3.sol"; +import { + CallerNotConfiguratorException, + CallerNotCreditManagerException, + CreditAccountIsInUseException, + MasterCreditAccountAlreadyDeployedException, + RegisteredCreditManagerOnlyException +} from "../../../interfaces/IExceptions.sol"; + +import {TestHelper} from "../../lib/helper.sol"; +import {AddressProviderV3ACLMock} from "../../mocks/core/AddressProviderV3ACLMock.sol"; + +import {AccountFactoryV3Harness, FactoryParams, QueuedAccount} from "./AccountFactoryV3Harness.sol"; + +/// @title Account factory V3 unit test +/// @notice U:[AF]: Unit tests for account factory +contract AccountFactoryV3UnitTest is TestHelper, IAccountFactoryV3Events { + AccountFactoryV3Harness accountFactory; + AddressProviderV3ACLMock addressProvider; + + address configurator; + address creditManager; + + function setUp() public { + configurator = makeAddr("CONFIGURATOR"); + creditManager = makeAddr("CREDIT_MANAGER"); + + vm.startPrank(configurator); + addressProvider = new AddressProviderV3ACLMock(); + addressProvider.addCreditManager(creditManager); + accountFactory = new AccountFactoryV3Harness(address(addressProvider)); + accountFactory.addCreditManager(creditManager); + vm.stopPrank(); + } + + /// @notice U:[AF-1]: External functions have correct access + function test_U_AF_01_external_functions_have_correct_access(address caller) public { + vm.startPrank(caller); + if (caller != creditManager) { + vm.expectRevert(CallerNotCreditManagerException.selector); + accountFactory.takeCreditAccount(0, 0); + } + if (caller != creditManager) { + vm.expectRevert(CallerNotCreditManagerException.selector); + accountFactory.returnCreditAccount(address(0)); + } + if (caller != configurator) { + vm.expectRevert(CallerNotConfiguratorException.selector); + accountFactory.addCreditManager(address(0)); + } + if (caller != configurator) { + vm.expectRevert(CallerNotConfiguratorException.selector); + accountFactory.rescue(address(0), address(0), bytes("")); + } + vm.stopPrank(); + } + + /// @notice U:[AF-2A]: `takeCreditAccount` works correctly when queue has no reusable accounts + function test_U_AF_02A_takeCreditAccount_works_correctly_when_queue_has_no_reusable_accounts(uint8 head, uint8 tail) + public + { + vm.assume(head <= tail); + FactoryParams memory fp = accountFactory.factoryParams(creditManager); + accountFactory.setFactoryParams(creditManager, fp.masterCreditAccount, head, tail); + accountFactory.initQueuedAccounts(creditManager, tail); + if (head < tail) { + accountFactory.setQueuedAccount(creditManager, head, address(0), uint40(block.timestamp + 1)); + } + + vm.expectEmit(false, true, false, false); + emit DeployCreditAccount(address(0), creditManager); + + vm.expectEmit(false, true, false, false); + emit TakeCreditAccount(address(0), creditManager); + + vm.prank(creditManager); + address creditAccount = accountFactory.takeCreditAccount(0, 0); + + assertNotEq(creditAccount, address(0), "Incorrect clone account"); + assertEq(CreditAccountV3(creditAccount).factory(), address(accountFactory), "Incorrect clone account's factory"); + assertEq( + CreditAccountV3(creditAccount).creditManager(), creditManager, "Incorrect cline deployed's creditManager" + ); + } + + /// @notice U:[AF-2B]: `takeCreditAccount` works correctly when queue has reusable accounts + function test_U_AF_02B_takeCreditAccount_works_correctly_when_queue_has_reusable_accounts( + address creditAccount, + uint8 head, + uint8 tail + ) public { + vm.assume(head < tail); + + FactoryParams memory fp = accountFactory.factoryParams(creditManager); + accountFactory.setFactoryParams(creditManager, fp.masterCreditAccount, head, tail); + accountFactory.initQueuedAccounts(creditManager, tail); + accountFactory.setQueuedAccount(creditManager, head, creditAccount, uint40(block.timestamp - 1)); + + vm.expectEmit(true, true, false, false); + emit TakeCreditAccount(creditAccount, creditManager); + + vm.prank(creditManager); + address result = accountFactory.takeCreditAccount(0, 0); + + assertEq(result, creditAccount, "Incorrect creditAccount"); + assertEq(accountFactory.factoryParams(creditManager).head, uint40(head) + 1, "Incorrect head"); + } + + /// @notice U:[AF-3]: `returnCreditAccount` works correctly + function test_U_AF_03_returnCreditAccount_works_correctly(address creditAccount, uint8 tail) public { + FactoryParams memory fp = accountFactory.factoryParams(creditManager); + accountFactory.setFactoryParams(creditManager, fp.masterCreditAccount, fp.head, tail); + accountFactory.initQueuedAccounts(creditManager, tail); + + vm.expectEmit(true, true, false, false); + emit ReturnCreditAccount(creditAccount, creditManager); + + vm.prank(creditManager); + accountFactory.returnCreditAccount(creditAccount); + + QueuedAccount memory qa = accountFactory.queuedAccounts(creditManager, tail); + assertEq(qa.creditAccount, creditAccount, "Incorrect creditAccount"); + assertEq(qa.reusableAfter, uint40(block.timestamp + 3 days), "Incorrect reusableAfter"); + assertEq(accountFactory.factoryParams(creditManager).tail, uint40(tail) + 1, "Incorrect tail"); + } + + /// @notice U:[AF-4A]: `addCreditManager` reverts on non-registered credit manager + function test_U_AF_04A_addCreditManager_reverts_on_non_registered_credit_manager(address manager) public { + vm.assume(manager != creditManager); + vm.expectRevert(RegisteredCreditManagerOnlyException.selector); + vm.prank(configurator); + accountFactory.addCreditManager(manager); + } + + /// @notice U:[AF-4B]: `addCreditManager` reverts on already added credit manager + function test_U_AF_04B_addCreditManager_reverts_on_already_added_credit_manager( + address creditAccount, + address manager + ) public { + vm.assume(manager != creditManager && creditAccount != address(0)); + + addressProvider.addCreditManager(manager); + accountFactory.setFactoryParams(manager, creditAccount, 0, 0); + + vm.expectRevert(MasterCreditAccountAlreadyDeployedException.selector); + vm.prank(configurator); + accountFactory.addCreditManager(manager); + } + + /// @notice U:[AF-4C]: `addCreditManager` works correctly + function test_U_AF_04C_addCreditManager_works_correctly(address manager) public { + addressProvider.addCreditManager(manager); + + vm.expectEmit(true, false, false, false); + emit AddCreditManager(manager, address(0)); + + vm.prank(configurator); + accountFactory.addCreditManager(manager); + + address account = accountFactory.factoryParams(manager).masterCreditAccount; + assertNotEq(account, address(0), "Incorrect master account"); + assertEq(CreditAccountV3(account).factory(), address(accountFactory), "Incorrect master account's factory"); + assertEq(CreditAccountV3(account).creditManager(), manager, "Incorrect master account's creditManager"); + } + + /// @notice U:[AF-5A]: `rescue` reverts on non-registered credit manager + function test_U_AF_05A_rescue_reverts_on_non_registered_credit_manager(address creditAccount, address manager) + public + { + vm.assume(creditAccount != address(vm) && manager != creditManager); + + vm.mockCall( + creditAccount, abi.encodeCall(CreditAccountV3(creditAccount).creditManager, ()), abi.encode(manager) + ); + + vm.expectRevert(RegisteredCreditManagerOnlyException.selector); + vm.prank(configurator); + accountFactory.rescue(creditAccount, address(0), bytes("")); + } + + /// @notice U:[AF-5B]: `rescue` reverts when credit account is in use + function test_U_AF_05B_rescue_reverts_when_credit_account_is_in_use(address creditAccount, address borrower) + public + { + vm.assume(creditAccount != address(vm) && borrower != address(0)); + + CreditAccountInfo memory info; + info.borrower = borrower; + vm.mockCall( + creditAccount, abi.encodeCall(CreditAccountV3(creditAccount).creditManager, ()), abi.encode(creditManager) + ); + vm.mockCall( + creditManager, + abi.encodeCall(CreditManagerV3(creditManager).creditAccountInfo, (creditAccount)), + abi.encode(info) + ); + + vm.expectRevert(CreditAccountIsInUseException.selector); + vm.prank(configurator); + accountFactory.rescue(creditAccount, address(0), bytes("")); + } + + /// @notice U:[AF-5C]: `rescue` works correctly + function test_U_AF_05C_rescue_works_correctly(address creditAccount, address target, bytes calldata data) public { + vm.assume(creditAccount != address(vm)); + + CreditAccountInfo memory info; + vm.mockCall( + creditAccount, abi.encodeCall(CreditAccountV3(creditAccount).creditManager, ()), abi.encode(creditManager) + ); + vm.mockCall( + creditManager, + abi.encodeCall(CreditManagerV3(creditManager).creditAccountInfo, (creditAccount)), + abi.encode(info) + ); + vm.mockCall(creditAccount, abi.encodeCall(CreditAccountV3(creditAccount).rescue, (target, data)), bytes("")); + + vm.expectCall(creditAccount, abi.encodeCall(CreditAccountV3(creditAccount).rescue, (target, data))); + vm.prank(configurator); + accountFactory.rescue(creditAccount, target, data); + } +} diff --git a/contracts/test/unit/core/AccountFactoryV3Harness.sol b/contracts/test/unit/core/AccountFactoryV3Harness.sol new file mode 100644 index 00000000..2993c719 --- /dev/null +++ b/contracts/test/unit/core/AccountFactoryV3Harness.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2023 +pragma solidity ^0.8.17; + +import {AccountFactoryV3, FactoryParams, QueuedAccount} from "../../../core/AccountFactoryV3.sol"; + +contract AccountFactoryV3Harness is AccountFactoryV3 { + constructor(address addressProvider) AccountFactoryV3(addressProvider) {} + + function queuedAccounts(address creditManager, uint256 index) external view returns (QueuedAccount memory) { + return _queuedAccounts[creditManager][index]; + } + + function initQueuedAccounts(address creditManager, uint256 tail) external { + QueuedAccount memory qa; + for (uint256 i; i < tail; ++i) { + _queuedAccounts[creditManager].push(qa); + } + } + + function setQueuedAccount(address creditManager, uint256 index, address creditAccount, uint40 reusableAfter) + external + { + _queuedAccounts[creditManager][index] = QueuedAccount(creditAccount, reusableAfter); + } + + function factoryParams(address creditManager) external view returns (FactoryParams memory) { + return _factoryParams[creditManager]; + } + + function setFactoryParams(address creditManager, address masterCreditAccount, uint40 head, uint40 tail) external { + _factoryParams[creditManager] = FactoryParams(masterCreditAccount, head, tail); + } +} diff --git a/contracts/test/unit/credit/CreditAccountV3.unit.t.sol b/contracts/test/unit/credit/CreditAccountV3.unit.t.sol new file mode 100644 index 00000000..2462e0a2 --- /dev/null +++ b/contracts/test/unit/credit/CreditAccountV3.unit.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2023 +pragma solidity ^0.8.17; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {CreditAccountV3} from "../../../credit/CreditAccountV3.sol"; +import {CallerNotAccountFactoryException, CallerNotCreditManagerException} from "../../../interfaces/IExceptions.sol"; + +import {TestHelper} from "../../lib/helper.sol"; + +/// @title Credit account V3 unit test +/// @notice U:[CA]: Unit tests for credit account +contract CreditAccountV3UnitTest is TestHelper { + CreditAccountV3 creditAccount; + + address factory; + address creditManager; + + function setUp() public { + factory = makeAddr("ACCOUNT_FACTORY"); + creditManager = makeAddr("CREDIT_MANAGER"); + + vm.prank(factory); + creditAccount = new CreditAccountV3(creditManager); + } + + /// @notice U:[CA-1]: Constructor sets correct values + function test_U_CA_01_constructor_sets_correct_values(address factory_, address creditManager_) public { + vm.assume(factory_ != creditManager_); + + vm.prank(factory_); + CreditAccountV3 creditAccount_ = new CreditAccountV3(creditManager_); + + assertEq(creditAccount_.factory(), factory_, "Incorrect factory"); + assertEq(creditAccount_.creditManager(), creditManager_, "Incorrect creditManager"); + } + + /// @notice U:[CA-2]: External functions have correct access + function test_U_CA_02_external_functions_have_correct_access(address caller) public { + vm.startPrank(caller); + if (caller != creditManager) { + vm.expectRevert(CallerNotCreditManagerException.selector); + creditAccount.safeTransfer({token: address(0), to: address(0), amount: 0}); + } + if (caller != creditManager) { + vm.expectRevert(CallerNotCreditManagerException.selector); + creditAccount.execute({target: address(0), data: bytes("")}); + } + if (caller != factory) { + vm.expectRevert(CallerNotAccountFactoryException.selector); + creditAccount.rescue({target: address(0), data: bytes("")}); + } + vm.stopPrank(); + } + + /// @notice U:[CA-3]: `safeTransfer` works correctly + function test_U_CA_03_safeTransfer_works_correctly(address token, address to, uint256 amount) public { + vm.assume(token != address(vm)); // just brilliant + vm.mockCall(token, abi.encodeCall(IERC20.transfer, (to, amount)), bytes("")); + vm.expectCall(token, abi.encodeCall(IERC20.transfer, (to, amount))); + vm.prank(creditManager); + creditAccount.safeTransfer({token: token, to: to, amount: amount}); + } + + /// @notice U:[CA-4]: `execute` works correctly + function test_U_CA_04_execute_works_correctly(address target, bytes memory data, bytes memory expResult) public { + vm.assume(target != address(vm)); + vm.mockCall(target, data, expResult); + vm.expectCall(target, data); + vm.prank(creditManager); + bytes memory result = creditAccount.execute(target, data); + assertEq(result, expResult, "Incorrect result"); + } + + /// @notice U:[CA-5]: `rescue` works correctly + function test_U_CA_05_rescue_works_correctly(address target, bytes memory data) public { + vm.assume(target != address(vm)); + vm.mockCall(target, data, bytes("")); + vm.expectCall(target, data); + vm.prank(factory); + creditAccount.rescue(target, data); + } +} diff --git a/contracts/test/unit/credit/CreditFacade.t.sol b/contracts/test/unit/credit/CreditFacade.t.sol deleted file mode 100644 index 16503c76..00000000 --- a/contracts/test/unit/credit/CreditFacade.t.sol +++ /dev/null @@ -1,2175 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; - -import {CreditFacadeV3} from "../../../credit/CreditFacadeV3.sol"; -import {CreditManagerV3} from "../../../credit/CreditManagerV3.sol"; - -import {BotList} from "../../../support/BotList.sol"; -import {AccountFactory} from "@gearbox-protocol/core-v2/contracts/core/AccountFactory.sol"; - -import "../../../interfaces/IAccountFactory.sol"; -import "../../../interfaces/ICreditAccount.sol"; -import "../../../interfaces/ICreditFacade.sol"; -import { - ICreditManagerV3, - ICreditManagerV3Events, - ClosureAction, - ManageDebtAction -} from "../../../interfaces/ICreditManagerV3.sol"; -import {AllowanceAction} from "../../../interfaces/ICreditConfiguratorV3.sol"; -import {ICreditFacadeEvents} from "../../../interfaces/ICreditFacade.sol"; -import {IDegenNFT, IDegenNFTExceptions} from "@gearbox-protocol/core-v2/contracts/interfaces/IDegenNFT.sol"; -import {IWithdrawalManager} from "../../../interfaces/IWithdrawalManager.sol"; - -// DATA -import {MultiCall, MultiCallOps} from "@gearbox-protocol/core-v2/contracts/libraries/MultiCall.sol"; -import {Balance} from "@gearbox-protocol/core-v2/contracts/libraries/Balances.sol"; - -import {CreditFacadeMulticaller, CreditFacadeCalls} from "../../../multicall/CreditFacadeCalls.sol"; - -// CONSTANTS - -import {LEVERAGE_DECIMALS} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; -import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; - -// TESTS - -import "../../lib/constants.sol"; -import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; -import {CreditFacadeTestHelper} from "../../helpers/CreditFacadeTestHelper.sol"; - -// EXCEPTIONS -import "../../../interfaces/IExceptions.sol"; - -// MOCKS -import {AdapterMock} from "../../mocks/adapters/AdapterMock.sol"; -import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/adapters/TargetContractMock.sol"; -import {ERC20BlacklistableMock} from "../../mocks/token/ERC20Blacklistable.sol"; - -// SUITES -import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; -import {Tokens} from "../../config/Tokens.sol"; -import {CreditFacadeTestSuite} from "../../suites/CreditFacadeTestSuite.sol"; -import {CreditConfig} from "../../config/CreditConfig.sol"; - -import "forge-std/console.sol"; - -uint256 constant WETH_TEST_AMOUNT = 5 * WAD; -uint16 constant REFERRAL_CODE = 23; - -/// @title CreditFacadeTest -/// @notice Designed for unit test purposes only -contract CreditFacadeTest is BalanceHelper, CreditFacadeTestHelper, ICreditManagerV3Events, ICreditFacadeEvents { - using CreditFacadeCalls for CreditFacadeMulticaller; - - AccountFactory accountFactory; - - TargetContractMock targetMock; - AdapterMock adapterMock; - - function setUp() public { - _setUp(Tokens.DAI); - } - - function _setUp(Tokens _underlying) internal { - _setUp(_underlying, false, false, false, 1); - } - - function _setUp( - Tokens _underlying, - bool withDegenNFT, - bool withExpiration, - bool supportQuotas, - uint8 accountFactoryVer - ) internal { - tokenTestSuite = new TokensTestSuite(); - tokenTestSuite.topUpWETH{value: 100 * WAD}(); - - CreditConfig creditConfig = new CreditConfig( - tokenTestSuite, - _underlying - ); - - cft = new CreditFacadeTestSuite({ _creditConfig: creditConfig, - supportQuotas: supportQuotas, - withDegenNFT: withDegenNFT, - withExpiration: withExpiration, - accountFactoryVer: accountFactoryVer}); - - underlying = tokenTestSuite.addressOf(_underlying); - creditManager = cft.creditManager(); - creditFacade = cft.creditFacade(); - creditConfigurator = cft.creditConfigurator(); - - accountFactory = cft.af(); - - targetMock = new TargetContractMock(); - adapterMock = new AdapterMock( - address(creditManager), - address(targetMock) - ); - - vm.prank(CONFIGURATOR); - creditConfigurator.allowContract(address(targetMock), address(adapterMock)); - - vm.label(address(adapterMock), "AdapterMock"); - vm.label(address(targetMock), "TargetContractMock"); - } - - /// - /// - /// HELPERS - /// - /// - - function _prepareForWETHTest() internal { - _prepareForWETHTest(USER); - } - - function _prepareForWETHTest(address tester) internal { - address weth = tokenTestSuite.addressOf(Tokens.WETH); - - vm.startPrank(tester); - if (tester.balance > 0) { - IWETH(weth).deposit{value: tester.balance}(); - } - - IERC20(weth).transfer(address(this), tokenTestSuite.balanceOf(Tokens.WETH, tester)); - - vm.stopPrank(); - expectBalance(Tokens.WETH, tester, 0); - - vm.deal(tester, WETH_TEST_AMOUNT); - } - - function _checkForWETHTest() internal { - _checkForWETHTest(USER); - } - - function _checkForWETHTest(address tester) internal { - expectBalance(Tokens.WETH, tester, WETH_TEST_AMOUNT); - - expectEthBalance(tester, 0); - } - - function _prepareMockCall() internal returns (bytes memory callData) { - vm.prank(CONFIGURATOR); - creditConfigurator.allowContract(address(targetMock), address(adapterMock)); - - callData = abi.encodeWithSignature("hello(string)", "world"); - } - - /// - /// - /// TESTS - /// - /// - - // TODO: ideas how to revert with ZA? - - // /// @dev [FA-1]: constructor reverts for zero address - // function test_FA_01_constructor_reverts_for_zero_address() public { - // vm.expectRevert(ZeroAddressException.selector); - // new CreditFacadeV3(address(0), address(0), address(0), false); - // } - - /// @dev [FA-1A]: constructor sets correct values - function test_FA_01A_constructor_sets_correct_values() public { - assertEq(address(creditFacade.creditManager()), address(creditManager), "Incorrect creditManager"); - assertEq(creditFacade.underlying(), underlying, "Incorrect underlying token"); - - assertEq(creditFacade.wethAddress(), creditManager.wethAddress(), "Incorrect wethAddress token"); - - assertEq(creditFacade.degenNFT(), address(0), "Incorrect degenNFT"); - - assertTrue(creditFacade.whitelisted() == false, "Incorrect whitelisted"); - - _setUp({ - _underlying: Tokens.DAI, - withDegenNFT: true, - withExpiration: false, - supportQuotas: false, - accountFactoryVer: 1 - }); - creditFacade = cft.creditFacade(); - - assertEq(creditFacade.degenNFT(), address(cft.degenNFT()), "Incorrect degenNFT"); - - assertTrue(creditFacade.whitelisted() == true, "Incorrect whitelisted"); - } - - // - // ALL FUNCTIONS REVERTS IF USER HAS NO ACCOUNT - // - - /// @dev [FA-2]: functions reverts if borrower has no account - function test_FA_02_functions_reverts_if_borrower_has_no_account() public { - vm.expectRevert(CreditAccountNotExistsException.selector); - vm.prank(USER); - creditFacade.closeCreditAccount(DUMB_ADDRESS, FRIEND, 0, false, multicallBuilder()); - - vm.expectRevert(CreditAccountNotExistsException.selector); - vm.prank(USER); - creditFacade.closeCreditAccount( - DUMB_ADDRESS, - FRIEND, - 0, - false, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) - }) - ) - ); - - vm.expectRevert(CreditAccountNotExistsException.selector); - vm.prank(USER); - creditFacade.liquidateCreditAccount(DUMB_ADDRESS, DUMB_ADDRESS, 0, false, multicallBuilder()); - - vm.expectRevert(CreditAccountNotExistsException.selector); - vm.prank(USER); - creditFacade.multicall( - DUMB_ADDRESS, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) - }) - ) - ); - - // vm.prank(CONFIGURATOR); - // creditConfigurator.allowContract(address(targetMock), address(adapterMock)); - - vm.expectRevert(CreditAccountNotExistsException.selector); - vm.prank(USER); - creditFacade.transferAccountOwnership(DUMB_ADDRESS, FRIEND); - } - - // - // ETH => WETH TESTS - // - function test_FA_03B_openCreditAccountMulticall_correctly_wraps_ETH() public { - /// - openCreditAccount - - _prepareForWETHTest(); - - vm.prank(USER); - creditFacade.openCreditAccount{value: WETH_TEST_AMOUNT}( - DAI_ACCOUNT_AMOUNT, - USER, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) - }) - ), - false, - 0 - ); - _checkForWETHTest(); - } - - function test_FA_03C_closeCreditAccount_correctly_wraps_ETH() public { - (address creditAccount,) = _openTestCreditAccount(); - - vm.roll(block.number + 1); - - _prepareForWETHTest(); - vm.prank(USER); - creditFacade.closeCreditAccount{value: WETH_TEST_AMOUNT}(creditAccount, USER, 0, false, multicallBuilder()); - _checkForWETHTest(); - } - - function test_FA_03D_liquidate_correctly_wraps_ETH() public { - (address creditAccount,) = _openTestCreditAccount(); - - vm.roll(block.number + 1); - - tokenTestSuite.burn(Tokens.DAI, creditAccount, tokenTestSuite.balanceOf(Tokens.DAI, creditAccount)); - - _prepareForWETHTest(LIQUIDATOR); - - tokenTestSuite.approve(Tokens.DAI, LIQUIDATOR, address(creditManager)); - - tokenTestSuite.mint(Tokens.DAI, LIQUIDATOR, DAI_ACCOUNT_AMOUNT); - - vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount{value: WETH_TEST_AMOUNT}( - creditAccount, LIQUIDATOR, 0, false, multicallBuilder() - ); - _checkForWETHTest(LIQUIDATOR); - } - - function test_FA_03F_multicall_correctly_wraps_ETH() public { - (address creditAccount,) = _openTestCreditAccount(); - - // MULTICALL - _prepareForWETHTest(); - - vm.prank(USER); - creditFacade.multicall{value: WETH_TEST_AMOUNT}( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) - }) - ) - ); - _checkForWETHTest(); - } - - // - // OPEN CREDIT ACCOUNT - // - - /// @dev [FA-4A]: openCreditAccount reverts for using addresses which is not allowed by transfer allowance - function test_FA_04A_openCreditAccount_reverts_for_using_addresses_which_is_not_allowed_by_transfer_allowance() - public - { - (uint256 minBorrowedAmount,) = creditFacade.debtLimits(); - - vm.startPrank(USER); - - MultiCall[] memory calls = multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) - }) - ); - vm.expectRevert(AccountTransferNotAllowedException.selector); - creditFacade.openCreditAccount(minBorrowedAmount, FRIEND, calls, false, 0); - - vm.stopPrank(); - } - - /// @dev [FA-4B]: openCreditAccount reverts if user has no NFT for degen mode - function test_FA_04B_openCreditAccount_reverts_for_non_whitelisted_account() public { - _setUp({ - _underlying: Tokens.DAI, - withDegenNFT: true, - withExpiration: false, - supportQuotas: false, - accountFactoryVer: 1 - }); - - (uint256 minBorrowedAmount,) = creditFacade.debtLimits(); - - vm.expectRevert(IDegenNFTExceptions.InsufficientBalanceException.selector); - - vm.prank(FRIEND); - creditFacade.openCreditAccount( - minBorrowedAmount, - FRIEND, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) - }) - ), - false, - 0 - ); - } - - /// @dev [FA-4C]: openCreditAccount opens account and burns token - function test_FA_04C_openCreditAccount_burns_token_in_whitelisted_mode() public { - _setUp({ - _underlying: Tokens.DAI, - withDegenNFT: true, - withExpiration: false, - supportQuotas: false, - accountFactoryVer: 1 - }); - - IDegenNFT degenNFT = IDegenNFT(creditFacade.degenNFT()); - - vm.prank(CONFIGURATOR); - degenNFT.mint(USER, 2); - - expectBalance(address(degenNFT), USER, 2); - - (address creditAccount,) = _openTestCreditAccount(); - - expectBalance(address(degenNFT), USER, 1); - - _closeTestCreditAccount(creditAccount); - - tokenTestSuite.mint(Tokens.DAI, USER, DAI_ACCOUNT_AMOUNT); - - vm.prank(USER); - creditFacade.openCreditAccount( - DAI_ACCOUNT_AMOUNT, - USER, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT)) - }) - ), - false, - 0 - ); - - expectBalance(address(degenNFT), USER, 0); - } - - // /// @dev [FA-5]: openCreditAccount sets correct values - // function test_FA_05_openCreditAccount_sets_correct_values() public { - // uint16 LEVERAGE = 300; // x3 - - // address expectedCreditAccountAddress = accountFactory.head(); - - // vm.prank(FRIEND); - // creditFacade.approveAccountTransfer(USER, true); - - // vm.expectCall( - // address(creditManager), - // abi.encodeCall( - // "openCreditAccount(uint256,address)", (DAI_ACCOUNT_AMOUNT * LEVERAGE) / LEVERAGE_DECIMALS, FRIEND - // ) - // ); - - // vm.expectEmit(true, true, false, true); - // emit OpenCreditAccount( - // FRIEND, expectedCreditAccountAddress, (DAI_ACCOUNT_AMOUNT * LEVERAGE) / LEVERAGE_DECIMALS, REFERRAL_CODE - // ); - - // vm.expectCall( - // address(creditManager), - // abi.encodeCall( - // "addCollateral(address,address,address,uint256)", - // USER, - // expectedCreditAccountAddress, - // underlying, - // DAI_ACCOUNT_AMOUNT - // ) - // ); - - // vm.expectEmit(true, true, false, true); - // emit AddCollateral(creditAccount, FRIEND, underlying, DAI_ACCOUNT_AMOUNT); - - // vm.prank(USER); - // creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, FRIEND, LEVERAGE, REFERRAL_CODE); - // } - - /// @dev [FA-7]: openCreditAccount and openCreditAccount reverts when debt increase is forbidden - function test_FA_07_openCreditAccountMulticall_reverts_if_borrowing_forbidden() public { - (uint256 minBorrowedAmount,) = creditFacade.debtLimits(); - - vm.prank(CONFIGURATOR); - creditConfigurator.forbidBorrowing(); - - MultiCall[] memory calls = multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) - }) - ); - - vm.expectRevert(BorrowedBlockLimitException.selector); - vm.prank(USER); - creditFacade.openCreditAccount(minBorrowedAmount, USER, calls, false, 0); - } - - /// @dev [FA-8]: openCreditAccount runs operations in correct order - function test_FA_08_openCreditAccountMulticall_runs_operations_in_correct_order() public { - vm.prank(FRIEND); - creditFacade.approveAccountTransfer(USER, true); - - RevocationPair[] memory revocations = new RevocationPair[](1); - - revocations[0] = RevocationPair({spender: address(this), token: underlying}); - - // tokenTestSuite.mint(Tokens.DAI, USER, WAD); - // tokenTestSuite.approve(Tokens.DAI, USER, address(creditManager)); - - address expectedCreditAccountAddress = accountFactory.head(); - - MultiCall[] memory calls = multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT)) - }), - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.revokeAdapterAllowances, (revocations)) - }) - ); - - // EXPECTED STACK TRACE & EVENTS - - vm.expectCall( - address(creditManager), - abi.encodeCall(ICreditManagerV3.openCreditAccount, (DAI_ACCOUNT_AMOUNT, FRIEND, false)) - ); - - vm.expectEmit(true, true, false, true); - emit OpenCreditAccount(expectedCreditAccountAddress, FRIEND, USER, DAI_ACCOUNT_AMOUNT, REFERRAL_CODE); - - vm.expectEmit(true, false, false, false); - emit StartMultiCall(expectedCreditAccountAddress); - - vm.expectCall( - address(creditManager), - abi.encodeCall( - ICreditManagerV3.addCollateral, (USER, expectedCreditAccountAddress, underlying, DAI_ACCOUNT_AMOUNT) - ) - ); - - vm.expectEmit(true, true, false, true); - emit AddCollateral(expectedCreditAccountAddress, underlying, DAI_ACCOUNT_AMOUNT); - - vm.expectCall( - address(creditManager), - abi.encodeCall(ICreditManagerV3.revokeAdapterAllowances, (expectedCreditAccountAddress, revocations)) - ); - - vm.expectEmit(false, false, false, true); - emit FinishMultiCall(); - - vm.expectCall( - address(creditManager), - abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, - (expectedCreditAccountAddress, 1, new uint256[](0), PERCENTAGE_FACTOR) - ) - ); - - vm.prank(USER); - creditFacade.openCreditAccount(DAI_ACCOUNT_AMOUNT, FRIEND, calls, false, REFERRAL_CODE); - } - - /// @dev [FA-9]: openCreditAccount cant open credit account with hf <1; - function test_FA_09_openCreditAccountMulticall_cant_open_credit_account_with_hf_less_one( - uint256 amount, - uint8 token1 - ) public { - vm.assume(amount > 10000 && amount < DAI_ACCOUNT_AMOUNT); - vm.assume(token1 > 0 && token1 < creditManager.collateralTokensCount()); - - tokenTestSuite.mint(Tokens.DAI, address(creditManager.poolService()), type(uint96).max); - - vm.prank(CONFIGURATOR); - creditConfigurator.setMaxDebtPerBlockMultiplier(type(uint8).max); - - vm.prank(CONFIGURATOR); - creditConfigurator.setLimits(1, type(uint96).max); - - (address collateral,) = creditManager.collateralTokens(token1); - - tokenTestSuite.mint(collateral, USER, type(uint96).max); - - tokenTestSuite.approve(collateral, USER, address(creditManager)); - - uint256 lt = creditManager.liquidationThresholds(collateral); - - uint256 twvUSD = cft.priceOracle().convertToUSD(amount * lt, collateral) - + cft.priceOracle().convertToUSD(DAI_ACCOUNT_AMOUNT * DEFAULT_UNDERLYING_LT, underlying); - - uint256 borrowedAmountUSD = cft.priceOracle().convertToUSD(DAI_ACCOUNT_AMOUNT * PERCENTAGE_FACTOR, underlying); - - console.log("T:", twvUSD); - console.log("T:", borrowedAmountUSD); - - bool shouldRevert = twvUSD < borrowedAmountUSD; - - if (shouldRevert) { - vm.expectRevert(NotEnoughCollateralException.selector); - } - - vm.prank(USER); - creditFacade.openCreditAccount( - DAI_ACCOUNT_AMOUNT, - USER, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (collateral, amount)) - }) - ), - false, - REFERRAL_CODE - ); - } - - /// @dev [FA-10]: decrease debt during openCreditAccount - function test_FA_10_decrease_debt_forbidden_during_openCreditAccount() public { - vm.expectRevert(abi.encodeWithSelector(NoPermissionException.selector, DECREASE_DEBT_PERMISSION)); - - vm.prank(USER); - - creditFacade.openCreditAccount( - DAI_ACCOUNT_AMOUNT, - USER, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.decreaseDebt, 812) - }) - ), - false, - REFERRAL_CODE - ); - } - - /// @dev [FA-11A]: openCreditAccount reverts if met borrowed limit per block - function test_FA_11A_openCreditAccount_reverts_if_met_borrowed_limit_per_block() public { - (uint128 _minDebt, uint128 _maxDebt) = creditFacade.debtLimits(); - - tokenTestSuite.mint(Tokens.DAI, address(cft.poolMock()), _maxDebt * 2); - - tokenTestSuite.mint(Tokens.DAI, USER, DAI_ACCOUNT_AMOUNT); - tokenTestSuite.mint(Tokens.DAI, FRIEND, DAI_ACCOUNT_AMOUNT); - - tokenTestSuite.approve(Tokens.DAI, USER, address(creditManager)); - tokenTestSuite.approve(Tokens.DAI, FRIEND, address(creditManager)); - - vm.roll(2); - - vm.prank(CONFIGURATOR); - creditConfigurator.setMaxDebtPerBlockMultiplier(1); - - MultiCall[] memory calls = multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT)) - }) - ); - - vm.prank(FRIEND); - creditFacade.openCreditAccount(_maxDebt - _minDebt, FRIEND, calls, false, 0); - - vm.expectRevert(BorrowedBlockLimitException.selector); - - vm.prank(USER); - creditFacade.openCreditAccount(_minDebt + 1, USER, calls, false, 0); - } - - /// @dev [FA-11B]: openCreditAccount reverts if amount < minAmount or amount > maxAmount - function test_FA_11B_openCreditAccount_reverts_if_amount_less_minBorrowedAmount_or_bigger_than_maxBorrowedAmount() - public - { - (uint128 minBorrowedAmount, uint128 maxBorrowedAmount) = creditFacade.debtLimits(); - - MultiCall[] memory calls = multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) - }) - ); - - vm.expectRevert(BorrowAmountOutOfLimitsException.selector); - vm.prank(USER); - creditFacade.openCreditAccount(minBorrowedAmount - 1, USER, calls, false, 0); - - vm.expectRevert(BorrowAmountOutOfLimitsException.selector); - vm.prank(USER); - creditFacade.openCreditAccount(maxBorrowedAmount + 1, USER, calls, false, 0); - } - - // - // CLOSE CREDIT ACCOUNT - // - - /// @dev [FA-12]: closeCreditAccount runs multicall operations in correct order - function test_FA_12_closeCreditAccount_runs_operations_in_correct_order() public { - (address creditAccount, uint256 balance) = _openTestCreditAccount(); - - bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); - - MultiCall[] memory calls = multicallBuilder( - MultiCall({target: address(adapterMock), callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))}) - ); - - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (creditAccount)) - ); - - vm.expectEmit(true, false, false, false); - emit StartMultiCall(creditAccount); - - vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.executeOrder, (DUMB_CALLDATA))); - - vm.expectEmit(true, false, false, true); - emit ExecuteOrder(address(targetMock)); - - vm.expectCall(creditAccount, abi.encodeCall(ICreditAccount.execute, (address(targetMock), DUMB_CALLDATA))); - - vm.expectCall(address(targetMock), DUMB_CALLDATA); - - vm.expectEmit(false, false, false, true); - emit FinishMultiCall(); - - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (address(1))) - ); - - // 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); - - // 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, true, calls); - - assertEq0(targetMock.callData(), DUMB_CALLDATA, "Incorrect calldata"); - } - - /// @dev [FA-13]: closeCreditAccount reverts on internal calls in multicall - function test_FA_13_closeCreditAccount_reverts_on_internal_call_in_multicall_on_closure() public { - /// 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(MultiCall({target: address(creditFacade), callData: DUMB_CALLDATA})) - // ); - } - - // - // LIQUIDATE CREDIT ACCOUNT - // - - /// @dev [FA-14]: liquidateCreditAccount reverts if hf > 1 - function test_FA_14_liquidateCreditAccount_reverts_if_hf_is_greater_than_1() public { - (address creditAccount,) = _openTestCreditAccount(); - - vm.expectRevert(CreditAccountNotLiquidatableException.selector); - - vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount(creditAccount, LIQUIDATOR, 0, true, multicallBuilder()); - } - - /// @dev [FA-15]: liquidateCreditAccount executes needed calls and emits events - function test_FA_15_liquidateCreditAccount_executes_needed_calls_and_emits_events() public { - (address creditAccount,) = _openTestCreditAccount(); - - bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); - - MultiCall[] memory calls = multicallBuilder( - MultiCall({target: address(adapterMock), callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))}) - ); - - _makeAccountsLiquitable(); - - // EXPECTED STACK TRACE & EVENTS - - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (creditAccount)) - ); - - vm.expectEmit(true, false, false, false); - emit StartMultiCall(creditAccount); - - vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.executeOrder, (DUMB_CALLDATA))); - - vm.expectEmit(true, false, false, false); - emit ExecuteOrder(address(targetMock)); - - vm.expectCall(creditAccount, abi.encodeCall(ICreditAccount.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.setCreditAccountForExternalCall, (address(1))) - ); - - // Total value = 2 * DAI_ACCOUNT_AMOUNT, cause we have x2 leverage - uint256 totalValue = 2 * DAI_ACCOUNT_AMOUNT; - uint256 debtWithInterest = DAI_ACCOUNT_AMOUNT; - - // vm.expectCall( - // address(creditManager), - // abi.encodeCall( - // ICreditManagerV3.closeCreditAccount, - // ( - // creditAccount, - // ClosureAction.LIQUIDATE_ACCOUNT, - // totalValue, - // LIQUIDATOR, - // FRIEND, - // 1, - // 10, - // debtWithInterest, - // true - // ) - // ) - // ); - - vm.expectEmit(true, true, true, true); - emit LiquidateCreditAccount(creditAccount, USER, LIQUIDATOR, FRIEND, ClosureAction.LIQUIDATE_ACCOUNT, 0); - - vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 10, true, calls); - } - - /// @dev [FA-15A]: Borrowing is prohibited after a liquidation with loss - function test_FA_15A_liquidateCreditAccount_prohibits_borrowing_on_loss() public { - (address creditAccount,) = _openTestCreditAccount(); - - uint8 maxDebtPerBlockMultiplier = creditFacade.maxDebtPerBlockMultiplier(); - - assertGt(maxDebtPerBlockMultiplier, 0, "SETUP: Increase debt is already enabled"); - - bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); - - MultiCall[] memory calls = multicallBuilder( - MultiCall({target: address(adapterMock), callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))}) - ); - - _makeAccountsLiquitable(); - - vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 10, true, calls); - - maxDebtPerBlockMultiplier = creditFacade.maxDebtPerBlockMultiplier(); - - assertEq(maxDebtPerBlockMultiplier, 0, "Increase debt wasn't forbidden after loss"); - } - - /// @dev [FA-15B]: CreditFacade is paused after too much cumulative loss from liquidations - function test_FA_15B_liquidateCreditAccount_pauses_CreditFacade_on_too_much_loss() public { - vm.prank(CONFIGURATOR); - creditConfigurator.setMaxCumulativeLoss(1); - - (address creditAccount,) = _openTestCreditAccount(); - - bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); - - MultiCall[] memory calls = multicallBuilder( - MultiCall({target: address(adapterMock), callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))}) - ); - - _makeAccountsLiquitable(); - - vm.prank(LIQUIDATOR); - creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 10, true, calls); - - assertTrue(creditFacade.paused(), "Credit manager was not paused"); - } - - function test_FA_16_liquidateCreditAccount_reverts_on_internal_call_in_multicall_on_closure() public { - /// TODO: Add all cases with different permissions! - - MultiCall[] memory calls = multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) - }) - ); - - (address creditAccount,) = _openTestCreditAccount(); - - _makeAccountsLiquitable(); - vm.expectRevert(abi.encodeWithSelector(NoPermissionException.selector, ADD_COLLATERAL_PERMISSION)); - - vm.prank(LIQUIDATOR); - - // It's used dumb calldata, cause all calls to creditFacade are forbidden - creditFacade.liquidateCreditAccount(creditAccount, FRIEND, 10, true, calls); - } - - // [FA-16A]: liquidateCreditAccount reverts when zero address is passed as to - function test_FA_16A_liquidateCreditAccount_reverts_on_zero_to_address() public { - bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); - - MultiCall[] memory calls = multicallBuilder( - MultiCall({target: address(adapterMock), callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))}) - ); - _openTestCreditAccount(); - - _makeAccountsLiquitable(); - vm.expectRevert(ZeroAddressException.selector); - - vm.prank(LIQUIDATOR); - - // It's used dumb calldata, cause all calls to creditFacade are forbidden - creditFacade.liquidateCreditAccount(USER, address(0), 10, true, calls); - } - - // // - // // INCREASE & DECREASE DEBT - // // - - /// @dev [FA-17]: increaseDebt executes function as expected - function test_FA_17_increaseDebt_executes_actions_as_expected() public { - (address creditAccount,) = _openTestCreditAccount(); - - vm.expectCall( - address(creditManager), - abi.encodeCall(ICreditManagerV3.manageDebt, (creditAccount, 512, 1, ManageDebtAction.INCREASE_DEBT)) - ); - - vm.expectCall( - address(creditManager), - abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, 1, new uint256[](0), PERCENTAGE_FACTOR) - ) - ); - - vm.expectEmit(true, false, false, true); - emit IncreaseDebt(creditAccount, 512); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.increaseDebt, (512)) - }) - ) - ); - } - - /// @dev [FA-18A]: increaseDebt revets if more than block limit - function test_FA_18A_increaseDebt_revets_if_more_than_block_limit() public { - (address creditAccount,) = _openTestCreditAccount(); - - uint8 maxDebtPerBlockMultiplier = creditFacade.maxDebtPerBlockMultiplier(); - (, uint128 maxDebt) = creditFacade.debtLimits(); - - vm.expectRevert(BorrowedBlockLimitException.selector); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.increaseDebt, (maxDebt * maxDebtPerBlockMultiplier + 1)) - }) - ) - ); - } - - /// @dev [FA-18B]: increaseDebt revets if more than maxBorrowedAmount - function test_FA_18B_increaseDebt_revets_if_more_than_block_limit() public { - (address creditAccount,) = _openTestCreditAccount(); - - (, uint128 maxBorrowedAmount) = creditFacade.debtLimits(); - - uint256 amount = maxBorrowedAmount - DAI_ACCOUNT_AMOUNT + 1; - - tokenTestSuite.mint(Tokens.DAI, address(cft.poolMock()), amount); - - vm.expectRevert(BorrowAmountOutOfLimitsException.selector); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.increaseDebt, (amount)) - }) - ) - ); - } - - /// @dev [FA-18C]: increaseDebt revets isIncreaseDebtForbidden is enabled - function test_FA_18C_increaseDebt_revets_isIncreaseDebtForbidden_is_enabled() public { - (address creditAccount,) = _openTestCreditAccount(); - - vm.prank(CONFIGURATOR); - creditConfigurator.forbidBorrowing(); - - vm.expectRevert(BorrowedBlockLimitException.selector); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.increaseDebt, (1)) - }) - ) - ); - } - - /// @dev [FA-18D]: increaseDebt reverts if there is a forbidden token on account - function test_FA_18D_increaseDebt_reverts_with_forbidden_tokens() public { - (address creditAccount,) = _openTestCreditAccount(); - - address link = tokenTestSuite.addressOf(Tokens.LINK); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.enableToken, (link)) - }) - ) - ); - - vm.prank(CONFIGURATOR); - creditConfigurator.forbidToken(link); - - vm.expectRevert(ForbiddenTokensException.selector); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.increaseDebt, (1)) - }) - ) - ); - } - - /// @dev [FA-19]: decreaseDebt executes function as expected - function test_FA_19_decreaseDebt_executes_actions_as_expected() public { - (address creditAccount,) = _openTestCreditAccount(); - - vm.expectCall( - address(creditManager), - abi.encodeCall(ICreditManagerV3.manageDebt, (creditAccount, 512, 1, ManageDebtAction.DECREASE_DEBT)) - ); - - vm.expectCall( - address(creditManager), - abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, 1, new uint256[](0), PERCENTAGE_FACTOR) - ) - ); - - vm.expectEmit(true, false, false, true); - emit DecreaseDebt(creditAccount, 512); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.decreaseDebt, (512)) - }) - ) - ); - } - - /// @dev [FA-20]:decreaseDebt revets if less than minBorrowedAmount - function test_FA_20_decreaseDebt_revets_if_less_than_minBorrowedAmount() public { - (address creditAccount,) = _openTestCreditAccount(); - - (uint128 minBorrowedAmount,) = creditFacade.debtLimits(); - - uint256 amount = DAI_ACCOUNT_AMOUNT - minBorrowedAmount + 1; - - tokenTestSuite.mint(Tokens.DAI, address(cft.poolMock()), amount); - - vm.expectRevert(BorrowAmountOutOfLimitsException.selector); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.decreaseDebt, (amount)) - }) - ) - ); - } - - // - // ADD COLLATERAL - // - - /// @dev [FA-21]: addCollateral executes function as expected - function test_FA_21_addCollateral_executes_actions_as_expected() public { - (address creditAccount,) = _openTestCreditAccount(); - - expectTokenIsEnabled(creditAccount, Tokens.USDC, false); - - address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - - tokenTestSuite.mint(Tokens.USDC, USER, 512); - tokenTestSuite.approve(Tokens.USDC, USER, address(creditManager)); - - vm.expectCall( - address(creditManager), - abi.encodeCall(ICreditManagerV3.addCollateral, (USER, creditAccount, usdcToken, 512)) - ); - - vm.expectEmit(true, true, false, true); - emit AddCollateral(creditAccount, usdcToken, 512); - - // TODO: change test - - MultiCall[] memory calls = multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (usdcToken, 512)) - }) - ); - - vm.prank(USER); - creditFacade.multicall(creditAccount, calls); - - expectBalance(Tokens.USDC, creditAccount, 512); - expectTokenIsEnabled(creditAccount, Tokens.USDC, true); - } - - /// @dev [FA-21C]: addCollateral calls checkEnabledTokensLength - function test_FA_21C_addCollateral_optimizes_enabled_tokens() public { - (address creditAccount,) = _openTestCreditAccount(); - - vm.prank(USER); - creditFacade.approveAccountTransfer(FRIEND, true); - - address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - - tokenTestSuite.mint(Tokens.USDC, FRIEND, 512); - tokenTestSuite.approve(Tokens.USDC, FRIEND, address(creditManager)); - - // vm.expectCall( - // address(creditManager), - // abi.encodeCall(ICreditManagerV3.checkEnabledTokensLength.selector, creditAccount) - // ); - - // vm.prank(FRIEND); - // creditFacade.addCollateral(USER, usdcToken, 512); - } - - // - // MULTICALL - // - - /// @dev [FA-22]: multicall reverts if calldata length is less than 4 bytes - function test_FA_22_multicall_reverts_if_calldata_length_is_less_than_4_bytes() public { - (address creditAccount,) = _openTestCreditAccount(); - - vm.expectRevert(IncorrectCallDataException.selector); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, multicallBuilder(MultiCall({target: address(creditFacade), callData: bytes("123")})) - ); - } - - /// @dev [FA-23]: multicall reverts for unknown methods - function test_FA_23_multicall_reverts_for_unknown_methods() public { - (address creditAccount,) = _openTestCreditAccount(); - - bytes memory DUMB_CALLDATA = abi.encodeWithSignature("hello(string)", "world"); - - vm.expectRevert(UnknownMethodException.selector); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, multicallBuilder(MultiCall({target: address(creditFacade), callData: DUMB_CALLDATA})) - ); - } - - /// @dev [FA-24]: multicall reverts for creditManager address - function test_FA_24_multicall_reverts_for_creditManager_address() public { - (address creditAccount,) = _openTestCreditAccount(); - - bytes memory DUMB_CALLDATA = abi.encodeWithSignature("hello(string)", "world"); - - vm.expectRevert(TargetContractNotAllowedException.selector); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, multicallBuilder(MultiCall({target: address(creditManager), callData: DUMB_CALLDATA})) - ); - } - - /// @dev [FA-25]: multicall reverts on non-adapter targets - function test_FA_25_multicall_reverts_for_non_adapters() public { - (address creditAccount,) = _openTestCreditAccount(); - - bytes memory DUMB_CALLDATA = abi.encodeWithSignature("hello(string)", "world"); - vm.expectRevert(TargetContractNotAllowedException.selector); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, multicallBuilder(MultiCall({target: DUMB_ADDRESS, callData: DUMB_CALLDATA})) - ); - } - - /// @dev [FA-26]: multicall addCollateral and oncreaseDebt works with creditFacade calls as expected - function test_FA_26_multicall_addCollateral_and_increase_debt_works_with_creditFacade_calls_as_expected() public { - (address creditAccount,) = _openTestCreditAccount(); - - address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - tokenTestSuite.mint(Tokens.USDC, USER, USDC_EXCHANGE_AMOUNT); - tokenTestSuite.approve(Tokens.USDC, USER, address(creditManager)); - - uint256 usdcMask = creditManager.getTokenMaskOrRevert(usdcToken); - - vm.expectEmit(true, true, false, true); - emit StartMultiCall(creditAccount); - - vm.expectCall( - address(creditManager), - abi.encodeCall(ICreditManagerV3.addCollateral, (USER, creditAccount, usdcToken, USDC_EXCHANGE_AMOUNT)) - ); - - vm.expectEmit(true, true, false, true); - emit AddCollateral(creditAccount, usdcToken, USDC_EXCHANGE_AMOUNT); - - vm.expectCall( - address(creditManager), - abi.encodeCall( - ICreditManagerV3.manageDebt, (creditAccount, 256, usdcMask | 1, ManageDebtAction.INCREASE_DEBT) - ) - ); - - vm.expectEmit(true, false, false, true); - emit IncreaseDebt(creditAccount, 256); - - vm.expectEmit(false, false, false, true); - emit FinishMultiCall(); - - vm.expectCall( - address(creditManager), - abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, 3, new uint256[](0), PERCENTAGE_FACTOR) - ) - ); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (usdcToken, USDC_EXCHANGE_AMOUNT)) - }), - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.increaseDebt, (256)) - }) - ) - ); - } - - /// @dev [FA-27]: multicall addCollateral and decreaseDebt works with creditFacade calls as expected - function test_FA_27_multicall_addCollateral_and_decreaseDebt_works_with_creditFacade_calls_as_expected() public { - (address creditAccount,) = _openTestCreditAccount(); - - address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - tokenTestSuite.mint(Tokens.USDC, USER, USDC_EXCHANGE_AMOUNT); - tokenTestSuite.approve(Tokens.USDC, USER, address(creditManager)); - - uint256 usdcMask = creditManager.getTokenMaskOrRevert(usdcToken); - - vm.expectEmit(true, true, false, true); - emit StartMultiCall(creditAccount); - - vm.expectCall( - address(creditManager), - abi.encodeCall(ICreditManagerV3.addCollateral, (USER, creditAccount, usdcToken, USDC_EXCHANGE_AMOUNT)) - ); - - vm.expectEmit(true, true, false, true); - emit AddCollateral(creditAccount, usdcToken, USDC_EXCHANGE_AMOUNT); - - vm.expectCall( - address(creditManager), - abi.encodeCall( - ICreditManagerV3.manageDebt, (creditAccount, 256, usdcMask | 1, ManageDebtAction.DECREASE_DEBT) - ) - ); - - vm.expectEmit(true, false, false, true); - emit DecreaseDebt(creditAccount, 256); - - vm.expectEmit(false, false, false, true); - emit FinishMultiCall(); - - vm.expectCall( - address(creditManager), - abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, 3, new uint256[](0), PERCENTAGE_FACTOR) - ) - ); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (usdcToken, USDC_EXCHANGE_AMOUNT)) - }), - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.decreaseDebt, 256) - }) - ) - ); - } - - /// @dev [FA-28]: multicall reverts for decrease opeartion after increase one - function test_FA_28_multicall_reverts_for_decrease_opeartion_after_increase_one() public { - (address creditAccount,) = _openTestCreditAccount(); - - vm.expectRevert(abi.encodeWithSelector(NoPermissionException.selector, DECREASE_DEBT_PERMISSION)); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.increaseDebt, 256) - }), - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.decreaseDebt, 256) - }) - ) - ); - } - - /// @dev [FA-29]: multicall works with adapters calls as expected - function test_FA_29_multicall_works_with_adapters_calls_as_expected() public { - (address creditAccount,) = _openTestCreditAccount(); - - bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); - - // TODO: add enable / disable cases - - MultiCall[] memory calls = multicallBuilder( - MultiCall({target: address(adapterMock), callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))}) - ); - - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (creditAccount)) - ); - - vm.expectEmit(true, true, false, true); - emit StartMultiCall(creditAccount); - - vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.executeOrder, (DUMB_CALLDATA))); - - vm.expectEmit(true, false, false, true); - emit ExecuteOrder(address(targetMock)); - - vm.expectCall(creditAccount, abi.encodeCall(ICreditAccount.execute, (address(targetMock), DUMB_CALLDATA))); - - vm.expectCall(address(targetMock), DUMB_CALLDATA); - - vm.expectEmit(false, false, false, true); - emit FinishMultiCall(); - - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (address(1))) - ); - - vm.expectCall( - address(creditManager), - abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, 1, new uint256[](0), PERCENTAGE_FACTOR) - ) - ); - - vm.prank(USER); - creditFacade.multicall(creditAccount, calls); - } - - // - // TRANSFER ACCOUNT OWNERSHIP - // - - // /// @dev [FA-32]: transferAccountOwnership reverts if "to" user doesn't provide allowance - /// TODO: CHANGE TO ALLOWANCE METHOD - // function test_FA_32_transferAccountOwnership_reverts_if_whitelisted_enabled() public { - // cft.testFacadeWithDegenNFT(); - // creditFacade = cft.creditFacade(); - - // vm.expectRevert(AccountTransferNotAllowedException.selector); - // vm.prank(USER); - // creditFacade.transferAccountOwnership(DUMB_ADDRESS); - // } - - /// @dev [FA-33]: transferAccountOwnership reverts if "to" user doesn't provide allowance - function test_FA_33_transferAccountOwnership_reverts_if_to_user_doesnt_provide_allowance() public { - (address creditAccount,) = _openTestCreditAccount(); - vm.expectRevert(AccountTransferNotAllowedException.selector); - - vm.prank(USER); - creditFacade.transferAccountOwnership(creditAccount, DUMB_ADDRESS); - } - - /// @dev [FA-34]: transferAccountOwnership reverts if hf less 1 - function test_FA_34_transferAccountOwnership_reverts_if_hf_less_1() public { - (address creditAccount,) = _openTestCreditAccount(); - - vm.prank(FRIEND); - creditFacade.approveAccountTransfer(USER, true); - - _makeAccountsLiquitable(); - - vm.expectRevert(CantTransferLiquidatableAccountException.selector); - - vm.prank(USER); - creditFacade.transferAccountOwnership(creditAccount, FRIEND); - } - - /// @dev [FA-35]: transferAccountOwnership transfers account if it's allowed - function test_FA_35_transferAccountOwnership_transfers_account_if_its_allowed() public { - (address creditAccount,) = _openTestCreditAccount(); - - vm.prank(FRIEND); - creditFacade.approveAccountTransfer(USER, true); - - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.transferAccountOwnership, (creditAccount, FRIEND)) - ); - - vm.expectEmit(true, true, false, false); - emit TransferAccount(creditAccount, USER, FRIEND); - - vm.prank(USER); - creditFacade.transferAccountOwnership(creditAccount, FRIEND); - - // assertEq( - // creditManager.getCreditAccountOrRevert(FRIEND), creditAccount, "Credit account was not properly transferred" - // ); - } - - /// @dev [FA-36]: checkAndUpdateBorrowedBlockLimit doesn't change block limit if maxBorrowedAmountPerBlock = type(uint128).max - function test_FA_36_checkAndUpdateBorrowedBlockLimit_doesnt_change_block_limit_if_set_to_max() public { - // vm.prank(CONFIGURATOR); - // creditConfigurator.setMaxDebtLimitPerBlock(type(uint128).max); - - // (uint64 blockLastUpdate, uint128 borrowedInBlock) = creditFacade.getTotalBorrowedInBlock(); - // assertEq(blockLastUpdate, 0, "Incorrect currentBlockLimit"); - // assertEq(borrowedInBlock, 0, "Incorrect currentBlockLimit"); - - // _openTestCreditAccount(); - - // (blockLastUpdate, borrowedInBlock) = creditFacade.getTotalBorrowedInBlock(); - // assertEq(blockLastUpdate, 0, "Incorrect currentBlockLimit"); - // assertEq(borrowedInBlock, 0, "Incorrect currentBlockLimit"); - } - - /// @dev [FA-37]: checkAndUpdateBorrowedBlockLimit doesn't change block limit if maxBorrowedAmountPerBlock = type(uint128).max - function test_FA_37_checkAndUpdateBorrowedBlockLimit_updates_block_limit_properly() public { - // (uint64 blockLastUpdate, uint128 borrowedInBlock) = creditFacade.getTotalBorrowedInBlock(); - - // assertEq(blockLastUpdate, 0, "Incorrect blockLastUpdate"); - // assertEq(borrowedInBlock, 0, "Incorrect borrowedInBlock"); - - // _openTestCreditAccount(); - - // (blockLastUpdate, borrowedInBlock) = creditFacade.getTotalBorrowedInBlock(); - - // assertEq(blockLastUpdate, block.number, "blockLastUpdate"); - // assertEq(borrowedInBlock, DAI_ACCOUNT_AMOUNT, "Incorrect borrowedInBlock"); - - // vm.prank(USER); - // creditFacade.multicall( - // multicallBuilder( - // MultiCall({ - // target: address(creditFacade), - // callData: abi.encodeCall(ICreditFacadeMulticall.increaseDebt, (DAI_EXCHANGE_AMOUNT)) - // }) - // ) - // ); - - // (blockLastUpdate, borrowedInBlock) = creditFacade.getTotalBorrowedInBlock(); - - // assertEq(blockLastUpdate, block.number, "blockLastUpdate"); - // assertEq(borrowedInBlock, DAI_ACCOUNT_AMOUNT + DAI_EXCHANGE_AMOUNT, "Incorrect borrowedInBlock"); - - // // switch to new block - // vm.roll(block.number + 1); - - // vm.prank(USER); - // creditFacade.multicall( - // multicallBuilder( - // MultiCall({ - // target: address(creditFacade), - // callData: abi.encodeCall(ICreditFacadeMulticall.increaseDebt, (DAI_EXCHANGE_AMOUNT)) - // }) - // ) - // ); - - // (blockLastUpdate, borrowedInBlock) = creditFacade.getTotalBorrowedInBlock(); - - // assertEq(blockLastUpdate, block.number, "blockLastUpdate"); - // assertEq(borrowedInBlock, DAI_EXCHANGE_AMOUNT, "Incorrect borrowedInBlock"); - } - - // - // APPROVE ACCOUNT TRANSFER - // - - /// @dev [FA-38]: approveAccountTransfer changes transfersAllowed - function test_FA_38_transferAccountOwnership_with_allowed_to_transfers_account() public { - assertTrue(creditFacade.transfersAllowed(USER, FRIEND) == false, "Transfer is unexpectedly allowed "); - - vm.expectEmit(true, true, false, true); - emit SetAccountTransferAllowance(USER, FRIEND, true); - - vm.prank(FRIEND); - creditFacade.approveAccountTransfer(USER, true); - - assertTrue(creditFacade.transfersAllowed(USER, FRIEND) == true, "Transfer is unexpectedly not allowed "); - - vm.expectEmit(true, true, false, true); - emit SetAccountTransferAllowance(USER, FRIEND, false); - - vm.prank(FRIEND); - creditFacade.approveAccountTransfer(USER, false); - assertTrue(creditFacade.transfersAllowed(USER, FRIEND) == false, "Transfer is unexpectedly allowed "); - } - - // - // ENABLE TOKEN - // - - /// @dev [FA-39]: enable token works as expected - function test_FA_39_enable_token_is_correct() public { - (address creditAccount,) = _openTestCreditAccount(); - - address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - expectTokenIsEnabled(creditAccount, Tokens.USDC, false); - - tokenTestSuite.mint(Tokens.USDC, creditAccount, 100); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.enableToken, (usdcToken)) - }) - ) - ); - - expectTokenIsEnabled(creditAccount, Tokens.USDC, true); - } - - // - // GETTERS - // - - /// @dev [FA-41]: calcTotalValue computes correctly - function test_FA_41_calcTotalValue_computes_correctly() public { - (address creditAccount,) = _openTestCreditAccount(); - - // AFTER OPENING CREDIT ACCOUNT - uint256 expectedTV = DAI_ACCOUNT_AMOUNT * 2; - uint256 expectedTWV = (DAI_ACCOUNT_AMOUNT * 2 * DEFAULT_UNDERLYING_LT) / PERCENTAGE_FACTOR; - - // (uint256 tv, uint256 tvw) = creditFacade.calcTotalValue(creditAccount); - - // assertEq(tv, expectedTV, "Incorrect total value for 1 asset"); - - // assertEq(tvw, expectedTWV, "Incorrect Threshold weighthed value for 1 asset"); - - // ADDS USDC BUT NOT ENABLES IT - address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - tokenTestSuite.mint(Tokens.USDC, creditAccount, 10 * 10 ** 6); - - // (tv, tvw) = creditFacade.calcTotalValue(creditAccount); - - // // tv and tvw shoul be the same until we deliberately enable USDC token - // assertEq(tv, expectedTV, "Incorrect total value for 1 asset"); - - // assertEq(tvw, expectedTWV, "Incorrect Threshold weighthed value for 1 asset"); - - // ENABLES USDC - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.enableToken, (usdcToken)) - }) - ) - ); - - expectedTV += 10 * WAD; - expectedTWV += (10 * WAD * 9000) / PERCENTAGE_FACTOR; - - // (tv, tvw) = creditFacade.calcTotalValue(creditAccount); - - // assertEq(tv, expectedTV, "Incorrect total value for 2 asset"); - - // assertEq(tvw, expectedTWV, "Incorrect Threshold weighthed value for 2 asset"); - - // 3 ASSET TEST: 10 DAI + 10 USDC + 0.01 WETH (3200 $/ETH) - addCollateral(Tokens.WETH, WAD / 100); - - expectedTV += (WAD / 100) * DAI_WETH_RATE; - expectedTWV += ((WAD / 100) * DAI_WETH_RATE * 8300) / PERCENTAGE_FACTOR; - - // (tv, tvw) = creditFacade.calcTotalValue(creditAccount); - - // assertEq(tv, expectedTV, "Incorrect total value for 3 asset"); - - // assertEq(tvw, expectedTWV, "Incorrect Threshold weighthed value for 3 asset"); - } - - /// @dev [FA-42]: calcCreditAccountHealthFactor computes correctly - function test_FA_42_calcCreditAccountHealthFactor_computes_correctly() public { - (address creditAccount,) = _openTestCreditAccount(); - - // AFTER OPENING CREDIT ACCOUNT - - uint256 expectedTV = DAI_ACCOUNT_AMOUNT * 2; - uint256 expectedTWV = (DAI_ACCOUNT_AMOUNT * 2 * DEFAULT_UNDERLYING_LT) / PERCENTAGE_FACTOR; - - uint256 expectedHF = (expectedTWV * PERCENTAGE_FACTOR) / DAI_ACCOUNT_AMOUNT; - - // assertEq(creditFacade.calcCreditAccountHealthFactor(creditAccount), expectedHF, "Incorrect health factor"); - - // ADDING USDC AS COLLATERAL - - addCollateral(Tokens.USDC, 10 * 10 ** 6); - - expectedTV += 10 * WAD; - expectedTWV += (10 * WAD * 9000) / PERCENTAGE_FACTOR; - - expectedHF = (expectedTWV * PERCENTAGE_FACTOR) / DAI_ACCOUNT_AMOUNT; - - // assertEq(creditFacade.calcCreditAccountHealthFactor(creditAccount), expectedHF, "Incorrect health factor"); - - // 3 ASSET: 10 DAI + 10 USDC + 0.01 WETH (3200 $/ETH) - addCollateral(Tokens.WETH, WAD / 100); - - expectedTV += (WAD / 100) * DAI_WETH_RATE; - expectedTWV += ((WAD / 100) * DAI_WETH_RATE * 8300) / PERCENTAGE_FACTOR; - - expectedHF = (expectedTWV * PERCENTAGE_FACTOR) / DAI_ACCOUNT_AMOUNT; - - // assertEq(creditFacade.calcCreditAccountHealthFactor(creditAccount), expectedHF, "Incorrect health factor"); - } - - /// CHECK IS ACCOUNT LIQUIDATABLE - - /// @dev [FA-44]: setContractToAdapter reverts if called non-configurator - function test_FA_44_config_functions_revert_if_called_non_configurator() public { - vm.expectRevert(CallerNotConfiguratorException.selector); - vm.prank(USER); - creditFacade.setDebtLimits(100, 100, 100); - - vm.expectRevert(CallerNotConfiguratorException.selector); - vm.prank(USER); - creditFacade.setBotList(FRIEND); - - vm.expectRevert(CallerNotConfiguratorException.selector); - vm.prank(USER); - creditFacade.setEmergencyLiquidator(DUMB_ADDRESS, AllowanceAction.ALLOW); - } - - /// CHECK SLIPPAGE PROTECTION - - /// [TODO]: add new test - - /// @dev [FA-45]: rrevertIfGetLessThan during multicalls works correctly - function test_FA_45_revertIfGetLessThan_works_correctly() public { - (address creditAccount,) = _openTestCreditAccount(); - - uint256 expectedDAI = 1000; - uint256 expectedLINK = 2000; - - address tokenLINK = tokenTestSuite.addressOf(Tokens.LINK); - - Balance[] memory expectedBalances = new Balance[](2); - expectedBalances[0] = Balance({token: underlying, balance: expectedDAI}); - - expectedBalances[1] = Balance({token: tokenLINK, balance: expectedLINK}); - - // TOKEN PREPARATION - tokenTestSuite.mint(Tokens.DAI, USER, expectedDAI * 3); - tokenTestSuite.mint(Tokens.LINK, USER, expectedLINK * 3); - - tokenTestSuite.approve(Tokens.DAI, USER, address(creditManager)); - tokenTestSuite.approve(Tokens.LINK, USER, address(creditManager)); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - CreditFacadeMulticaller(address(creditFacade)).revertIfReceivedLessThan(expectedBalances), - CreditFacadeMulticaller(address(creditFacade)).addCollateral(underlying, expectedDAI), - CreditFacadeMulticaller(address(creditFacade)).addCollateral(tokenLINK, expectedLINK) - ) - ); - - for (uint256 i = 0; i < 2; i++) { - vm.prank(USER); - vm.expectRevert( - abi.encodeWithSelector( - BalanceLessThanMinimumDesiredException.selector, ((i == 0) ? underlying : tokenLINK) - ) - ); - - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.revertIfReceivedLessThan, (expectedBalances)) - }), - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall( - ICreditFacadeMulticall.addCollateral, (underlying, (i == 0) ? expectedDAI - 1 : expectedDAI) - ) - }), - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall( - ICreditFacadeMulticall.addCollateral, (tokenLINK, (i == 0) ? expectedLINK : expectedLINK - 1) - ) - }) - ) - ); - } - } - - /// @dev [FA-45A]: rrevertIfGetLessThan everts if called twice - function test_FA_45A_revertIfGetLessThan_reverts_if_called_twice() public { - uint256 expectedDAI = 1000; - - Balance[] memory expectedBalances = new Balance[](1); - expectedBalances[0] = Balance({token: underlying, balance: expectedDAI}); - - (address creditAccount,) = _openTestCreditAccount(); - vm.prank(USER); - vm.expectRevert(ExpectedBalancesAlreadySetException.selector); - - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.revertIfReceivedLessThan, (expectedBalances)) - }), - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.revertIfReceivedLessThan, (expectedBalances)) - }) - ) - ); - } - - /// CREDIT FACADE WITH EXPIRATION - - /// @dev [FA-46]: openCreditAccount and openCreditAccount no longer work if the CreditFacadeV3 is expired - function test_FA_46_openCreditAccount_reverts_on_expired_CreditFacade() public { - _setUp({ - _underlying: Tokens.DAI, - withDegenNFT: false, - withExpiration: true, - supportQuotas: false, - accountFactoryVer: 1 - }); - - vm.warp(block.timestamp + 1); - - vm.expectRevert(NotAllowedAfterExpirationException.selector); - - vm.prank(USER); - creditFacade.openCreditAccount( - DAI_ACCOUNT_AMOUNT, - USER, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) - }) - ), - false, - 0 - ); - } - - /// @dev [FA-47]: liquidateExpiredCreditAccount should not work before the CreditFacadeV3 is expired - function test_FA_47_liquidateExpiredCreditAccount_reverts_before_expiration() public { - _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()); - } - - /// @dev [FA-48]: liquidateExpiredCreditAccount should not work when expiration is set to zero (i.e. CreditFacadeV3 is non-expiring) - function test_FA_48_liquidateExpiredCreditAccount_reverts_on_CreditFacade_with_no_expiration() public { - _openTestCreditAccount(); - - // vm.expectRevert(CantLiquidateNonExpiredException.selector); - - // vm.prank(LIQUIDATOR); - // creditFacade.liquidateExpiredCreditAccount(USER, LIQUIDATOR, 0, false, multicallBuilder()); - } - - /// @dev [FA-49]: liquidateExpiredCreditAccount works correctly and emits events - function test_FA_49_liquidateExpiredCreditAccount_works_correctly_after_expiration() public { - _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( - 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.calcCreditAccountAccruedInterest(creditAccount); - - // (, uint256 remainingFunds,,) = creditManager.calcClosePayments( - // balance, ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT, borrowedAmount, borrowedAmountWithInterest - // ); - - // // EXPECTED STACK TRACE & EVENTS - - // vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (creditAccount))); - - // vm.expectEmit(true, false, false, false); - // emit StartMultiCall(creditAccount); - - // vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.executeOrder, (DUMB_CALLDATA))); - - // vm.expectEmit(true, false, false, false); - // emit ExecuteOrder(address(targetMock)); - - // vm.expectCall(creditAccount, abi.encodeCall(ICreditAccount.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.setCreditAccountForExternalCall, (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); - } - - /// - /// ENABLE TOKEN - /// - - /// @dev [FA-53]: enableToken works as expected in a multicall - function test_FA_53_enableToken_works_as_expected_multicall() public { - (address creditAccount,) = _openTestCreditAccount(); - - address token = tokenTestSuite.addressOf(Tokens.USDC); - - // vm.expectCall( - // address(creditManager), abi.encodeCall(ICreditManagerV3.checkAndEnableToken.selector, token) - // ); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.enableToken, (token)) - }) - ) - ); - - expectTokenIsEnabled(creditAccount, Tokens.USDC, true); - } - - /// @dev [FA-54]: disableToken works as expected in a multicall - function test_FA_54_disableToken_works_as_expected_multicall() public { - (address creditAccount,) = _openTestCreditAccount(); - - address token = tokenTestSuite.addressOf(Tokens.USDC); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.enableToken, (token)) - }) - ) - ); - - // vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.disableToken.selector, token)); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.disableToken, (token)) - }) - ) - ); - - expectTokenIsEnabled(creditAccount, Tokens.USDC, false); - } - - // /// @dev [FA-56]: liquidateCreditAccount correctly uses BlacklistHelper during liquidations - // function test_FA_56_liquidateCreditAccount_correctly_handles_blacklisted_borrowers() public { - // _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(IWithdrawalManager.isBlacklisted, (usdc, USER))); - - // vm.expectCall( - // address(creditManager), abi.encodeCall(ICreditManagerV3.transferAccountOwnership, (USER, blacklistHelper)) - // ); - - // vm.expectCall(blacklistHelper, abi.encodeCall(IWithdrawalManager.addWithdrawal, (usdc, USER, expectedAmount))); - - // vm.expectEmit(true, false, false, true); - // emit UnderlyingSentToBlacklistHelper(USER, expectedAmount); - - // vm.prank(LIQUIDATOR); - // creditFacade.liquidateCreditAccount(USER, FRIEND, 0, true, multicallBuilder()); - - // assertEq(IWithdrawalManager(blacklistHelper).claimable(usdc, USER), expectedAmount, "Incorrect claimable amount"); - - // vm.prank(USER); - // IWithdrawalManager(blacklistHelper).claim(usdc, FRIEND2); - - // assertEq(tokenTestSuite.balanceOf(Tokens.USDC, FRIEND2), expectedAmount, "Transferred amount incorrect"); - // } - - // /// @dev [FA-57]: openCreditAccount reverts when the borrower is blacklisted on a blacklistable underlying - // function test_FA_57_openCreditAccount_reverts_on_blacklisted_borrower() public { - // _setUp(Tokens.USDC); - - // cft.testFacadeWithBlacklistHelper(); - - // creditFacade = cft.creditFacade(); - - // address usdc = tokenTestSuite.addressOf(Tokens.USDC); - - // ERC20BlacklistableMock(usdc).setBlacklisted(USER, true); - - // vm.expectRevert(NotAllowedForBlacklistedAddressException.selector); - - // vm.prank(USER); - // creditFacade.openCreditAccount( - // USDC_ACCOUNT_AMOUNT, - // USER, - // multicallBuilder( - // MultiCall({ - // target: address(creditFacade), - // callData: abi.encodeCall(ICreditFacadeMulticall.addCollateral, (underlying, DAI_ACCOUNT_AMOUNT / 4)) - // }) - // ), - // 0 - // ); - // } - - /// @dev [FA-58]: botMulticall works correctly - function test_FA_58_botMulticall_works_correctly() public { - (address creditAccount,) = _openTestCreditAccount(); - - BotList botList = new BotList(address(cft.addressProvider())); - - vm.prank(CONFIGURATOR); - creditConfigurator.setBotList(address(botList)); - - /// ???? - address bot = address(new TargetContractMock()); - - vm.prank(USER); - botList.setBotPermissions(bot, type(uint192).max); - - bytes memory DUMB_CALLDATA = adapterMock.dumbCallData(); - - MultiCall[] memory calls = multicallBuilder( - MultiCall({target: address(adapterMock), callData: abi.encodeCall(AdapterMock.dumbCall, (0, 0))}) - ); - - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (creditAccount)) - ); - - vm.expectEmit(true, true, false, true); - emit StartMultiCall(creditAccount); - - vm.expectCall(address(creditManager), abi.encodeCall(ICreditManagerV3.executeOrder, (DUMB_CALLDATA))); - - vm.expectEmit(true, false, false, true); - emit ExecuteOrder(address(targetMock)); - - vm.expectCall(creditAccount, abi.encodeCall(ICreditAccount.execute, (address(targetMock), DUMB_CALLDATA))); - - vm.expectCall(address(targetMock), DUMB_CALLDATA); - - vm.expectEmit(false, false, false, true); - emit FinishMultiCall(); - - vm.expectCall( - address(creditManager), abi.encodeCall(ICreditManagerV3.setCreditAccountForExternalCall, (address(1))) - ); - - vm.expectCall( - address(creditManager), - abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, 1, new uint256[](0), PERCENTAGE_FACTOR) - ) - ); - - vm.prank(bot); - creditFacade.botMulticall(creditAccount, calls); - - vm.expectRevert(NotApprovedBotException.selector); - creditFacade.botMulticall( - creditAccount, multicallBuilder(MultiCall({target: address(adapterMock), callData: DUMB_CALLDATA})) - ); - - vm.prank(CONFIGURATOR); - botList.setBotForbiddenStatus(bot, true); - - vm.expectRevert(NotApprovedBotException.selector); - vm.prank(bot); - creditFacade.botMulticall(creditAccount, calls); - } - - /// @dev [FA-59]: setFullCheckParams performs correct full check after multicall - function test_FA_59_setFullCheckParams_correctly_passes_params_to_fullCollateralCheck() public { - (address creditAccount,) = _openTestCreditAccount(); - - uint256[] memory collateralHints = new uint256[](1); - collateralHints[0] = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)); - - uint256 enabledTokensMap = creditManager.enabledTokensMaskOf(creditAccount); - - vm.expectCall( - address(creditManager), - abi.encodeCall( - ICreditManagerV3.fullCollateralCheck, (creditAccount, enabledTokensMap, collateralHints, 10001) - ) - ); - - vm.prank(USER); - creditFacade.multicall( - creditAccount, - multicallBuilder( - MultiCall({ - target: address(creditFacade), - callData: abi.encodeCall(ICreditFacadeMulticall.setFullCheckParams, (collateralHints, 10001)) - }) - ) - ); - } - - // - // EMERGENCY LIQUIDATIONS - // - - /// @dev [FA-62]: addEmergencyLiquidator correctly sets value - function test_FA_62_setEmergencyLiquidator_works_correctly() public { - vm.prank(address(creditConfigurator)); - creditFacade.setEmergencyLiquidator(DUMB_ADDRESS, AllowanceAction.ALLOW); - - assertTrue(creditFacade.canLiquidateWhilePaused(DUMB_ADDRESS), "Value was not set"); - - vm.prank(address(creditConfigurator)); - creditFacade.setEmergencyLiquidator(DUMB_ADDRESS, AllowanceAction.FORBID); - - assertTrue(!creditFacade.canLiquidateWhilePaused(DUMB_ADDRESS), "Value was is still set"); - } -} diff --git a/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol b/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol new file mode 100644 index 00000000..04f4731e --- /dev/null +++ b/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +import "../../../interfaces/IAddressProviderV3.sol"; +import {AddressProviderV3ACLMock} from "../../mocks/core/AddressProviderV3ACLMock.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; + +import {CreditFacadeV3} from "../../../credit/CreditFacadeV3.sol"; +import {CreditManagerV3} from "../../../credit/CreditManagerV3.sol"; +import {CreditManagerMock} from "../../mocks/credit/CreditManagerMock.sol"; +import {DegenNFTMock} from "../../mocks/token/DegenNFTMock.sol"; + +import {BotList} from "../../../support/BotList.sol"; + +import "../../../interfaces/ICreditFacade.sol"; +import {ICreditManagerV3, ClosureAction, ManageDebtAction} from "../../../interfaces/ICreditManagerV3.sol"; +import {AllowanceAction} from "../../../interfaces/ICreditConfiguratorV3.sol"; +import {ICreditFacadeEvents} from "../../../interfaces/ICreditFacade.sol"; +import {IDegenNFT, IDegenNFTExceptions} from "@gearbox-protocol/core-v2/contracts/interfaces/IDegenNFT.sol"; + +// DATA +import {MultiCall, MultiCallOps} from "@gearbox-protocol/core-v2/contracts/libraries/MultiCall.sol"; +import {Balance} from "@gearbox-protocol/core-v2/contracts/libraries/Balances.sol"; + +import {CreditFacadeMulticaller, CreditFacadeCalls} from "../../../multicall/CreditFacadeCalls.sol"; + +// CONSTANTS +import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; + +// TESTS + +import "../../lib/constants.sol"; +import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; + +// EXCEPTIONS +import "../../../interfaces/IExceptions.sol"; + +// SUITES +import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; +import {Tokens} from "../../config/Tokens.sol"; + +import "forge-std/console.sol"; + +uint256 constant WETH_TEST_AMOUNT = 5 * WAD; +uint16 constant REFERRAL_CODE = 23; + +/// @title CreditFacadeTest +/// @notice Designed for unit test purposes only +contract CreditFacadeUnitTest is BalanceHelper, ICreditFacadeEvents { + using CreditFacadeCalls for CreditFacadeMulticaller; + + IAddressProviderV3 addressProvider; + + CreditFacadeV3 creditFacade; + CreditManagerMock creditManagerMock; + + DegenNFTMock degenNFTMock; + bool whitelisted; + + bool expirable; + + modifier notExpirableCase() { + _notExpirable(); + _; + } + + modifier expirableCase() { + _expirable(); + _; + } + + modifier allExpirableCases() { + uint256 snapshot = vm.snapshot(); + _notExpirable(); + _; + vm.revertTo(snapshot); + + _expirable(); + _; + } + + modifier withoutDegenNFT() { + _withoutDegenNFT(); + _; + } + + modifier withDegenNFT() { + _withDegenNFT(); + _; + } + + modifier allDegenNftCases() { + uint256 snapshot = vm.snapshot(); + + _withoutDegenNFT(); + _; + vm.revertTo(snapshot); + + _withDegenNFT(); + _; + } + + function setUp() public { + tokenTestSuite = new TokensTestSuite(); + + tokenTestSuite.topUpWETH{value: 100 * WAD}(); + + addressProvider = new AddressProviderV3ACLMock(); + + addressProvider.setAddress(AP_WETH_TOKEN, tokenTestSuite.addressOf(Tokens.WETH), false); + + creditManagerMock = new CreditManagerMock({_addressProvider: address(addressProvider), _pool: address(0)}); + } + + function _withoutDegenNFT() internal { + degenNFTMock = DegenNFTMock(address(0)); + } + + function _withDegenNFT() internal { + whitelisted = true; + degenNFTMock = new DegenNFTMock(); + } + + function _notExpirable() internal { + expirable = false; + creditFacade = new CreditFacadeV3(address(creditManagerMock), address(degenNFTMock), expirable); + } + + function _expirable() internal { + expirable = true; + creditFacade = new CreditFacadeV3(address(creditManagerMock), address(degenNFTMock), expirable); + } + + /// @dev U:[FA-1]: constructor sets correct values + function test_U_FA_01_constructor_sets_correct_values() public allDegenNftCases allExpirableCases { + assertEq(address(creditFacade.creditManager()), address(creditManagerMock), "Incorrect creditManager"); + + assertEq(creditFacade.weth(), creditManagerMock.weth(), "Incorrect weth token"); + + assertEq(creditFacade.wethGateway(), creditManagerMock.wethGateway(), "Incorrect weth gateway"); + + assertEq(creditFacade.degenNFT(), address(degenNFTMock), "Incorrect degenNFTMock"); + + assertTrue(creditFacade.whitelisted() == whitelisted, "Incorrect whitelisted"); + } +} diff --git a/contracts/test/unit/credit/CreditFacadeV3Harness.sol b/contracts/test/unit/credit/CreditFacadeV3Harness.sol new file mode 100644 index 00000000..06d6d01d --- /dev/null +++ b/contracts/test/unit/credit/CreditFacadeV3Harness.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.8.17; + +import {CreditFacadeV3} from "../../../credit/CreditFacadeV3.sol"; +import {IPriceOracleV2} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; +import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; + +contract CreditFacadeV3Harness is CreditFacadeV3 { + constructor(address _creditManager, address _degenNFT, bool _expirable) + CreditFacadeV3(_creditManager, _degenNFT, _expirable) + {} + + function setReentrancy(uint8 _status) external { + _reentrancyStatus = _status; + } +} diff --git a/contracts/test/unit/credit/CreditManager.t.sol b/contracts/test/unit/credit/CreditManager.t.sol deleted file mode 100644 index b34745dc..00000000 --- a/contracts/test/unit/credit/CreditManager.t.sol +++ /dev/null @@ -1,2528 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; - -import {IAddressProvider} from "@gearbox-protocol/core-v2/contracts/interfaces/IAddressProvider.sol"; -import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; - -import {AccountFactory} from "@gearbox-protocol/core-v2/contracts/core/AccountFactory.sol"; -import {ICreditAccount} from "@gearbox-protocol/core-v2/contracts/interfaces/ICreditAccount.sol"; -import { - ICreditManagerV3, - ICreditManagerV3Events, - ClosureAction, - CollateralTokenData, - ManageDebtAction -} from "../../../interfaces/ICreditManagerV3.sol"; - -import {IPriceOracleV2, IPriceOracleV2Ext} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; -import {IWETHGateway} from "../../../interfaces/IWETHGateway.sol"; -import {IWithdrawalManager} from "../../../interfaces/IWithdrawalManager.sol"; - -import {CreditManagerV3} from "../../../credit/CreditManagerV3.sol"; - -import {IPoolService} from "@gearbox-protocol/core-v2/contracts/interfaces/IPoolService.sol"; - -import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ERC20Mock} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/ERC20Mock.sol"; -import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; - -// LIBS & TRAITS -import {BitMask} from "../../../libraries/BitMask.sol"; -// TESTS - -import "../../lib/constants.sol"; -import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; - -// EXCEPTIONS -import {TokenAlreadyAddedException} from "../../../interfaces/IExceptions.sol"; - -// MOCKS -import {PriceFeedMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/oracles/PriceFeedMock.sol"; -import {PoolServiceMock} from "../../mocks/pool/PoolServiceMock.sol"; -import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/adapters/TargetContractMock.sol"; -import { - ERC20ApproveRestrictedRevert, - ERC20ApproveRestrictedFalse -} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/ERC20ApproveRestricted.sol"; - -// SUITES -import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; -import {Tokens} from "../../config/Tokens.sol"; -import {CreditManagerTestSuite} from "../../suites/CreditManagerTestSuite.sol"; - -import {CreditManagerTestInternal} from "../../mocks/credit/CreditManagerTestInternal.sol"; - -import {CreditConfig} from "../../config/CreditConfig.sol"; - -// EXCEPTIONS -import "../../../interfaces/IExceptions.sol"; - -import {Test} from "forge-std/Test.sol"; -import "forge-std/console.sol"; - -/// @title AddressRepository -/// @notice Stores addresses of deployed contracts -contract CreditManagerTest is Test, ICreditManagerV3Events, BalanceHelper { - using BitMask for uint256; - - CreditManagerTestSuite cms; - - IAddressProvider addressProvider; - IWETH wethToken; - - AccountFactory af; - CreditManagerV3 creditManager; - PoolServiceMock poolMock; - IPriceOracleV2 priceOracle; - IWETHGateway wethGateway; - IWithdrawalManager withdrawalManager; - ACL acl; - address underlying; - - CreditConfig creditConfig; - - function setUp() public { - tokenTestSuite = new TokensTestSuite(); - - tokenTestSuite.topUpWETH{value: 100 * WAD}(); - _connectCreditManagerSuite(Tokens.DAI, false); - } - - /// - /// HELPERS - - function _connectCreditManagerSuite(Tokens t, bool internalSuite) internal { - creditConfig = new CreditConfig(tokenTestSuite, t); - cms = new CreditManagerTestSuite(creditConfig, internalSuite, false, 1); - - acl = cms.acl(); - - addressProvider = cms.addressProvider(); - af = cms.af(); - - poolMock = cms.poolMock(); - withdrawalManager = cms.withdrawalManager(); - - creditManager = cms.creditManager(); - - priceOracle = creditManager.priceOracle(); - underlying = creditManager.underlying(); - wethGateway = IWETHGateway(creditManager.wethGateway()); - } - - /// @dev Opens credit account for testing management functions - function _openCreditAccount() - internal - returns ( - uint256 borrowedAmount, - uint256 cumulativeIndexLastUpdate, - uint256 cumulativeIndexAtClose, - address creditAccount - ) - { - return cms.openCreditAccount(); - } - - function expectTokenIsEnabled(address creditAccount, Tokens t, bool expectedState) internal { - bool state = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(t)) - & creditManager.enabledTokensMaskOf(creditAccount) != 0; - assertTrue( - state == expectedState, - string( - abi.encodePacked( - "Token ", - tokenTestSuite.symbols(t), - state ? " enabled as not expected" : " not enabled as expected " - ) - ) - ); - } - - function mintBalance(address creditAccount, Tokens t, uint256 amount, bool enable) internal { - tokenTestSuite.mint(t, creditAccount, amount); - // if (enable) { - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(t)); - // } - } - - function _addAndEnableTokens(address creditAccount, uint256 numTokens, uint256 balance) internal { - for (uint256 i = 0; i < numTokens; i++) { - ERC20Mock t = new ERC20Mock("new token", "nt", 18); - PriceFeedMock pf = new PriceFeedMock(10**8, 8); - - vm.startPrank(CONFIGURATOR); - creditManager.addToken(address(t)); - IPriceOracleV2Ext(address(priceOracle)).addPriceFeed(address(t), address(pf)); - creditManager.setCollateralTokenData(address(t), 8000, 8000, type(uint40).max, 0); - vm.stopPrank(); - - t.mint(creditAccount, balance); - - // creditManager.checkAndEnableToken(address(t)); - } - } - - function _getRandomBits(uint256 ones, uint256 zeros, uint256 randomValue) - internal - pure - returns (bool[] memory result, uint256 breakPoint) - { - if ((ones + zeros) == 0) { - result = new bool[](0); - breakPoint = 0; - return (result, breakPoint); - } - - uint256 onesCurrent = ones; - uint256 zerosCurrent = zeros; - - result = new bool[](ones + zeros); - uint256 i = 0; - - while (onesCurrent + zerosCurrent > 0) { - uint256 rand = uint256(keccak256(abi.encodePacked(randomValue))) % (onesCurrent + zerosCurrent); - if (rand < onesCurrent) { - result[i] = true; - onesCurrent--; - } else { - result[i] = false; - zerosCurrent--; - } - - i++; - } - - if (ones > 0) { - uint256 breakpointCounter = (uint256(keccak256(abi.encodePacked(randomValue))) % (ones)) + 1; - - for (uint256 j = 0; j < result.length; j++) { - if (result[j]) { - breakpointCounter--; - } - - if (breakpointCounter == 0) { - breakPoint = j; - break; - } - } - } - } - - function enableTokensMoreThanLimit(address creditAccount) internal { - uint256 maxAllowedEnabledTokenLength = creditManager.maxAllowedEnabledTokenLength(); - _addAndEnableTokens(creditAccount, maxAllowedEnabledTokenLength, 2); - } - - function _openAccountAndTransferToCF() internal returns (address creditAccount) { - (,,, creditAccount) = _openCreditAccount(); - creditManager.transferAccountOwnership(creditAccount, address(this)); - } - - function _baseFullCollateralCheck(address creditAccount) internal { - // TODO: CHANGE - creditManager.fullCollateralCheck(creditAccount, 0, new uint256[](0), 10000); - } - - /// - /// - /// TESTS - /// - /// - /// @dev [CM-1]: credit manager reverts if were called non-creditFacade - function test_CM_01_constructor_sets_correct_values() public { - creditManager = new CreditManagerV3(address(poolMock), address(withdrawalManager)); - - assertEq(address(creditManager.poolService()), address(poolMock), "Incorrect poolSerivice"); - - assertEq(address(creditManager.pool()), address(poolMock), "Incorrect pool"); - - assertEq(creditManager.underlying(), tokenTestSuite.addressOf(Tokens.DAI), "Incorrect underlying"); - - (address token, uint16 lt) = creditManager.collateralTokens(0); - - assertEq(token, tokenTestSuite.addressOf(Tokens.DAI), "Incorrect underlying"); - - assertEq( - creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)), - 1, - "Incorrect token mask for underlying token" - ); - - assertEq(lt, 0, "Incorrect LT for underlying"); - - assertEq(creditManager.wethAddress(), addressProvider.getWethToken(), "Incorrect WETH token"); - - assertEq(address(creditManager.wethGateway()), addressProvider.getWETHGateway(), "Incorrect WETH Gateway"); - - assertEq(address(creditManager.priceOracle()), addressProvider.getPriceOracle(), "Incorrect Price oracle"); - - assertEq(address(creditManager.creditConfigurator()), address(this), "Incorrect creditConfigurator"); - } - - /// @dev [CM-2]:credit account management functions revert if were called non-creditFacade - /// Functions list: - /// - openCreditAccount - /// - closeCreditAccount - /// - manadgeDebt - /// - addCollateral - /// - transferOwnership - /// All these functions have creditFacadeOnly modifier - function test_CM_02_credit_account_management_functions_revert_if_not_called_by_creditFacadeCall() public { - assertEq(creditManager.creditFacade(), address(this)); - - vm.startPrank(USER); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.openCreditAccount(200000, address(this), false); - - // vm.expectRevert(CallerNotCreditFacadeException.selector); - // creditManager.closeCreditAccount( - // DUMB_ADDRESS, ClosureAction.LIQUIDATE_ACCOUNT, 0, DUMB_ADDRESS, DUMB_ADDRESS, type(uint256).max, false - // ); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.manageDebt(DUMB_ADDRESS, 100, 0, ManageDebtAction.INCREASE_DEBT); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.addCollateral(DUMB_ADDRESS, DUMB_ADDRESS, DUMB_ADDRESS, 100); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.transferAccountOwnership(DUMB_ADDRESS, DUMB_ADDRESS); - - vm.stopPrank(); - } - - /// @dev [CM-3]:credit account execution functions revert if were called non-creditFacade & non-adapters - /// Functions list: - /// - approveCreditAccount - /// - executeOrder - /// - checkAndEnableToken - /// - fullCollateralCheck - /// - disableToken - /// - changeEnabledTokens - function test_CM_03_credit_account_execution_functions_revert_if_not_called_by_creditFacade_or_adapters() public { - assertEq(creditManager.creditFacade(), address(this)); - - vm.startPrank(USER); - - vm.expectRevert(CallerNotAdapterException.selector); - creditManager.approveCreditAccount(DUMB_ADDRESS, 100); - - vm.expectRevert(CallerNotAdapterException.selector); - creditManager.executeOrder(bytes("0")); - - vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.fullCollateralCheck(DUMB_ADDRESS, 0, new uint256[](0), 10000); - - vm.stopPrank(); - } - - /// @dev [CM-4]:credit account configuration functions revert if were called non-configurator - /// Functions list: - /// - addToken - /// - setParams - /// - setLiquidationThreshold - /// - setForbidMask - /// - setContractAllowance - /// - upgradeContracts - /// - setCreditConfigurator - /// - addEmergencyLiquidator - /// - removeEmergenceLiquidator - function test_CM_04_credit_account_configurator_functions_revert_if_not_called_by_creditConfigurator() public { - assertEq(creditManager.creditFacade(), address(this)); - - vm.startPrank(USER); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.addToken(DUMB_ADDRESS); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setParams(0, 0, 0, 0, 0); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setCollateralTokenData(DUMB_ADDRESS, 0, 0, 0, 0); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setContractAllowance(DUMB_ADDRESS, DUMB_ADDRESS); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setCreditFacade(DUMB_ADDRESS); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setPriceOracle(DUMB_ADDRESS); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setCreditConfigurator(DUMB_ADDRESS); - - vm.expectRevert(CallerNotConfiguratorException.selector); - creditManager.setMaxEnabledTokens(255); - - vm.stopPrank(); - } - - // TODO: REMOVE OUTDATED - // /// @dev [CM-5]:credit account management+execution functions revert if were called non-creditFacade - // /// Functions list: - // /// - openCreditAccount - // /// - closeCreditAccount - // /// - manadgeDebt - // /// - addCollateral - // /// - transferOwnership - // /// All these functions have whenNotPaused modifier - // function test_CM_05_pause_pauses_management_functions() public { - // address root = acl.owner(); - // vm.prank(root); - - // acl.addPausableAdmin(root); - - // vm.prank(root); - // creditManager.pause(); - - // assertEq(creditManager.creditFacade(), address(this)); - - // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // creditManager.openCreditAccount(200000, address(this)); - - // // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // // creditManager.closeCreditAccount( - // // DUMB_ADDRESS, ClosureAction.LIQUIDATE_ACCOUNT, 0, DUMB_ADDRESS, DUMB_ADDRESS, type(uint256).max, false - // // ); - - // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // creditManager.manageDebt(DUMB_ADDRESS, 100, ManageDebtAction.INCREASE_DEBT); - - // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // creditManager.addCollateral(DUMB_ADDRESS, DUMB_ADDRESS, DUMB_ADDRESS, 100); - - // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // creditManager.transferAccountOwnership(DUMB_ADDRESS, DUMB_ADDRESS); - - // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // creditManager.approveCreditAccount(DUMB_ADDRESS, DUMB_ADDRESS, 100); - - // vm.expectRevert(bytes(PAUSABLE_ERROR)); - // creditManager.executeOrder(DUMB_ADDRESS, bytes("dd")); - // } - - // - // REVERTS IF CREDIT ACCOUNT NOT EXISTS - // - - /// @dev [CM-6A]: management function reverts if account not exists - /// Functions list: - /// - getCreditAccountOrRevert - /// - closeCreditAccount - /// - transferOwnership - - function test_CM_06A_management_functions_revert_if_account_does_not_exist() public { - // vm.expectRevert(CreditAccountNotExistsException.selector); - // creditManager.getCreditAccountOrRevert(USER); - - // vm.expectRevert(CreditAccountNotExistsException.selector); - // creditManager.closeCreditAccount( - // USER, ClosureAction.LIQUIDATE_ACCOUNT, 0, DUMB_ADDRESS, DUMB_ADDRESS, type(uint256).max, false - // ); - - vm.expectRevert(CreditAccountNotExistsException.selector); - creditManager.transferAccountOwnership(USER, DUMB_ADDRESS); - } - - /// @dev [CM-6A]: external call functions revert when the Credit Facade has no account - /// Functions list: - /// - executeOrder - /// - approveCreditAccount - function test_CM_06B_extenrnal_ca_only_functions_revert_when_ec_is_not_set() public { - address token = tokenTestSuite.addressOf(Tokens.DAI); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - vm.prank(ADAPTER); - vm.expectRevert(ExternalCallCreditAccountNotSetException.selector); - creditManager.approveCreditAccount(token, 100); - - // / TODO: decide about test - vm.prank(ADAPTER); - vm.expectRevert(ExternalCallCreditAccountNotSetException.selector); - creditManager.executeOrder(bytes("dd")); - } - - /// - /// OPEN CREDIT ACCOUNT - /// - - /// @dev [CM-7]: openCreditAccount reverts if zero address or address exists - function test_CM_07_openCreditAccount_reverts_if_address_exists() public { - // // Existing address case - // creditManager.openCreditAccount(1, USER); - // vm.expectRevert(UserAlreadyHasAccountException.selector); - // creditManager.openCreditAccount(1, USER); - } - - /// @dev [CM-8]: openCreditAccount sets correct values and transfers tokens from pool - function test_CM_08_openCreditAccount_sets_correct_values_and_transfers_tokens_from_pool() public { - address expectedCreditAccount = AccountFactory(addressProvider.getAccountFactory()).head(); - - uint256 blockAtOpen = block.number; - uint256 cumulativeAtOpen = 1012; - poolMock.setCumulative_RAY(cumulativeAtOpen); - - // Existing address case - address creditAccount = creditManager.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, false); - assertEq(creditAccount, expectedCreditAccount, "Incorrecct credit account address"); - - (uint256 debt, uint256 cumulativeIndexLastUpdate,,,,) = creditManager.creditAccountInfo(creditAccount); - - assertEq(debt, DAI_ACCOUNT_AMOUNT, "Incorrect borrowed amount set in CA"); - assertEq(cumulativeIndexLastUpdate, cumulativeAtOpen, "Incorrect cumulativeIndexLastUpdate set in CA"); - - assertEq(ICreditAccount(creditAccount).since(), blockAtOpen, "Incorrect since set in CA"); - - expectBalance(Tokens.DAI, creditAccount, DAI_ACCOUNT_AMOUNT); - assertEq(poolMock.lendAmount(), DAI_ACCOUNT_AMOUNT, "Incorrect DAI_ACCOUNT_AMOUNT in Pool call"); - assertEq(poolMock.lendAccount(), creditAccount, "Incorrect credit account in lendCreditAccount call"); - // assertEq(creditManager.creditAccounts(USER), creditAccount, "Credit account is not associated with user"); - assertEq(creditManager.enabledTokensMaskOf(creditAccount), 0, "Incorrect enabled token mask"); - } - - // - // CLOSE CREDIT ACCOUNT - // - - /// @dev [CM-9]: closeCreditAccount returns credit account to factory and - /// remove borrower from creditAccounts mapping - function test_CM_09_close_credit_account_returns_credit_account_and_remove_borrower_from_map() public { - (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); - - assertTrue( - creditAccount != AccountFactory(addressProvider.getAccountFactory()).tail(), - "credit account is already in tail!" - ); - - // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount - tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); - - // Increase block number cause it's forbidden to close credit account in the same block - vm.roll(block.number + 1); - - // creditManager.closeCreditAccount( - // creditAccount, ClosureAction.CLOSE_ACCOUNT, 0, USER, USER, 0, 0, DAI_ACCOUNT_AMOUNT, false - // ); - - assertEq( - creditAccount, - AccountFactory(addressProvider.getAccountFactory()).tail(), - "credit account is not in accountFactory tail!" - ); - - // vm.expectRevert(CreditAccountNotExistsException.selector); - // creditManager.getCreditAccountOrRevert(USER); - } - - /// @dev [CM-10]: 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_CM_10_close_credit_account_returns_underlying_token_if_not_liquidated() public { - ( - uint256 borrowedAmount, - uint256 cumulativeIndexLastUpdate, - uint256 cumulativeIndexAtClose, - address creditAccount - ) = _openCreditAccount(); - - uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(poolMock)); - - // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount - tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); - - uint256 interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; - - (uint16 feeInterest,,,,) = creditManager.fees(); - - uint256 profit = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - - uint256 amountToPool = borrowedAmount + interestAccrued + profit; - - vm.expectCall( - address(poolMock), - abi.encodeWithSelector(IPoolService.repayCreditAccount.selector, borrowedAmount, profit, 0) - ); - - // (uint256 remainingFunds, uint256 loss) = creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.CLOSE_ACCOUNT, - // 0, - // USER, - // FRIEND, - // 1, - // 0, - // DAI_ACCOUNT_AMOUNT + interestAccrued, - // false - // ); - - // assertEq(remainingFunds, 0, "Remaining funds is not zero!"); - - // assertEq(loss, 0, "Loss is not zero"); - - expectBalance(Tokens.DAI, creditAccount, 1); - - expectBalance(Tokens.DAI, address(poolMock), poolBalanceBefore + amountToPool); - - expectBalance(Tokens.DAI, FRIEND, 2 * borrowedAmount - amountToPool - 1, "Incorrect amount were paid back"); - } - - /// @dev [CM-11]: 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_CM_11_close_credit_account_charges_caller_if_underlying_token_not_enough() public { - ( - uint256 borrowedAmount, - uint256 cumulativeIndexLastUpdate, - uint256 cumulativeIndexAtClose, - address creditAccount - ) = _openCreditAccount(); - - uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(poolMock)); - - // Transfer funds to USER account to be able to cover extra cost - tokenTestSuite.mint(Tokens.DAI, USER, borrowedAmount); - - uint256 interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; - - (uint16 feeInterest,,,,) = creditManager.fees(); - - uint256 profit = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - - uint256 amountToPool = borrowedAmount + interestAccrued + profit; - - vm.expectCall( - address(poolMock), - abi.encodeWithSelector(IPoolService.repayCreditAccount.selector, borrowedAmount, profit, 0) - ); - - // (uint256 remainingFunds, uint256 loss) = creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.CLOSE_ACCOUNT, - // 0, - // USER, - // FRIEND, - // 1, - // 0, - // DAI_ACCOUNT_AMOUNT + interestAccrued, - // false - // ); - // assertEq(remainingFunds, 0, "Remaining funds is not zero!"); - - // assertEq(loss, 0, "Loss is not zero"); - - expectBalance(Tokens.DAI, creditAccount, 1, "Credit account balance != 1"); - - expectBalance(Tokens.DAI, address(poolMock), poolBalanceBefore + amountToPool); - - expectBalance(Tokens.DAI, USER, 2 * borrowedAmount - amountToPool - 1, "Incorrect amount were paid back"); - - expectBalance(Tokens.DAI, FRIEND, 0, "Incorrect amount were paid back"); - } - - /// @dev [CM-12]: closeCreditAccount 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_CM_12_close_credit_account_charges_caller_if_underlying_token_not_enough() public { - for (uint256 i = 0; i < 2; i++) { - uint256 friendBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, FRIEND); - - ClosureAction action = i == 1 ? ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT : ClosureAction.LIQUIDATE_ACCOUNT; - uint256 interestAccrued; - uint256 borrowedAmount; - address creditAccount; - - { - uint256 cumulativeIndexLastUpdate; - uint256 cumulativeIndexAtClose; - (borrowedAmount, cumulativeIndexLastUpdate, cumulativeIndexAtClose, creditAccount) = - _openCreditAccount(); - - interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; - } - - uint256 poolBalanceBefore = tokenTestSuite.balanceOf(Tokens.DAI, address(poolMock)); - uint256 discount; - - { - (,, uint16 liquidationDiscount,, uint16 liquidationDiscountExpired) = creditManager.fees(); - discount = action == ClosureAction.LIQUIDATE_ACCOUNT ? liquidationDiscount : liquidationDiscountExpired; - } - - // uint256 totalValue = borrowedAmount; - uint256 amountToPool = (borrowedAmount * discount) / PERCENTAGE_FACTOR; - - { - uint256 loss = borrowedAmount + interestAccrued - amountToPool; - - vm.expectCall( - address(poolMock), - abi.encodeWithSelector(IPoolService.repayCreditAccount.selector, borrowedAmount, 0, loss) - ); - } - { - uint256 a = borrowedAmount + interestAccrued; - - // (uint256 remainingFunds,) = creditManager.closeCreditAccount( - // creditAccount, action, borrowedAmount, LIQUIDATOR, FRIEND, 1, 0, a, false - // ); - } - - expectBalance(Tokens.DAI, creditAccount, 1, "Credit account balance != 1"); - - expectBalance(Tokens.DAI, address(poolMock), poolBalanceBefore + amountToPool); - - expectBalance( - Tokens.DAI, - FRIEND, - friendBalanceBefore + (borrowedAmount * (PERCENTAGE_FACTOR - discount)) / PERCENTAGE_FACTOR - - (i == 2 ? 0 : 1), - "Incorrect amount were paid to liqiudator friend address" - ); - } - } - - /// @dev [CM-13]: openCreditAccount 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_CM_13_close_credit_account_charges_caller_if_underlying_token_not_enough() public { - for (uint256 i = 0; i < 2; i++) { - setUp(); - uint256 borrowedAmount; - address creditAccount; - - uint256 expectedRemainingFunds = 100 * WAD; - - uint256 profit; - uint256 amountToPool; - uint256 totalValue; - uint256 interestAccrued; - { - uint256 cumulativeIndexLastUpdate; - uint256 cumulativeIndexAtClose; - (borrowedAmount, cumulativeIndexLastUpdate, cumulativeIndexAtClose, creditAccount) = - _openCreditAccount(); - - interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; - - uint16 feeInterest; - uint16 feeLiquidation; - uint16 liquidationDiscount; - - { - (feeInterest,,,,) = creditManager.fees(); - } - - { - uint16 feeLiquidationNormal; - uint16 feeLiquidationExpired; - - (, feeLiquidationNormal,, feeLiquidationExpired,) = creditManager.fees(); - - feeLiquidation = (i == 0 || i == 2) ? feeLiquidationNormal : feeLiquidationExpired; - } - - { - uint16 liquidationDiscountNormal; - uint16 liquidationDiscountExpired; - - (feeInterest,, liquidationDiscountNormal,, liquidationDiscountExpired) = creditManager.fees(); - - liquidationDiscount = i == 1 ? liquidationDiscountExpired : liquidationDiscountNormal; - } - - uint256 profitInterest = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - - amountToPool = borrowedAmount + 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(poolMock)); - - 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, borrowedAmount, "creditAccount has incorrect initial balance"); - - vm.expectCall( - address(poolMock), - abi.encodeWithSelector(IPoolService.repayCreditAccount.selector, borrowedAmount, profit, 0) - ); - - uint256 remainingFunds; - - { - uint256 loss; - - uint256 a = borrowedAmount + interestAccrued; - // (remainingFunds, loss) = creditManager.closeCreditAccount( - // creditAccount, - // i == 1 ? ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT : ClosureAction.LIQUIDATE_ACCOUNT, - // totalValue, - // LIQUIDATOR, - // FRIEND, - // 1, - // 0, - // a, - // 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(poolMock), poolBalanceBefore + amountToPool, "INCORRECT POOL BALANCE"); - } - - expectBalance( - Tokens.DAI, - LIQUIDATOR, - totalValue + borrowedAmount - amountToPool - remainingFunds - 1, - "Incorrect amount were paid to lqiudaidator" - ); - } - } - - /// @dev [CM-14]: 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_CM_14_close_credit_account_with_nonzero_skipTokenMask_sends_correct_tokens() public { - (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); - creditManager.transferAccountOwnership(creditAccount, address(this)); - - tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); - tokenTestSuite.mint(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.WETH)); - - tokenTestSuite.mint(Tokens.USDC, creditAccount, USDC_EXCHANGE_AMOUNT); - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.USDC)); - - tokenTestSuite.mint(Tokens.LINK, creditAccount, LINK_EXCHANGE_AMOUNT); - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.LINK)); - - uint256 wethTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - uint256 usdcTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)); - uint256 linkTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - - creditManager.transferAccountOwnership(creditAccount, USER); - - // creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.CLOSE_ACCOUNT, - // 0, - // USER, - // FRIEND, - // wethTokenMask | usdcTokenMask | linkTokenMask, - // wethTokenMask | usdcTokenMask, - // DAI_ACCOUNT_AMOUNT, - // 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 [CM-16]: closeCreditAccount sends ETH for WETH creditManger to borrower - /// CASE: CLOSURE - /// Underlying token: WETH - function test_CM_16_close_weth_credit_account_sends_eth_to_borrower() public { - // It takes "clean" address which doesn't holds any assets - - _connectCreditManagerSuite(Tokens.WETH, false); - - /// CLOSURE CASE - ( - uint256 borrowedAmount, - uint256 cumulativeIndexLastUpdate, - uint256 cumulativeIndexAtClose, - address creditAccount - ) = _openCreditAccount(); - - // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount - tokenTestSuite.mint(Tokens.WETH, creditAccount, borrowedAmount); - - uint256 interestAccrued = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate - borrowedAmount; - - // creditManager.closeCreditAccount(USER, ClosureAction.CLOSE_ACCOUNT, 0, USER, USER, 0, true); - - // creditManager.closeCreditAccount( - // creditAccount, ClosureAction.CLOSE_ACCOUNT, 0, USER, USER, 1, 0, borrowedAmount + interestAccrued, true - // ); - - expectBalance(Tokens.WETH, creditAccount, 1); - - (uint16 feeInterest,,,,) = creditManager.fees(); - - uint256 profit = (interestAccrued * feeInterest) / PERCENTAGE_FACTOR; - - uint256 amountToPool = borrowedAmount + interestAccrued + profit; - - assertEq( - wethGateway.balanceOf(USER), - 2 * borrowedAmount - amountToPool - 1, - "Incorrect amount deposited on wethGateway" - ); - } - - /// @dev [CM-17]: closeCreditAccount sends ETH for WETH creditManger to borrower - /// CASE: CLOSURE - /// Underlying token: DAI - function test_CM_17_close_dai_credit_account_sends_eth_to_borrower() public { - /// CLOSURE CASE - (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); - creditManager.transferAccountOwnership(creditAccount, address(this)); - - // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount - tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); - - // 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)); - - creditManager.transferAccountOwnership(creditAccount, USER); - // creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.CLOSE_ACCOUNT, - // 0, - // USER, - // USER, - // wethTokenMask | daiTokenMask, - // 0, - // borrowedAmount, - // true - // ); - - expectBalance(Tokens.WETH, creditAccount, 1); - - assertEq(wethGateway.balanceOf(USER), WETH_EXCHANGE_AMOUNT - 1, "Incorrect amount deposited on wethGateway"); - } - - /// @dev [CM-18]: closeCreditAccount sends ETH for WETH creditManger to borrower - /// CASE: LIQUIDATION - function test_CM_18_close_credit_account_sends_eth_to_liquidator_and_weth_to_borrower() public { - /// 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, false); - - /// CLOSURE CASE - (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); - - // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount - tokenTestSuite.mint(Tokens.WETH, creditAccount, borrowedAmount); - - uint256 totalValue = borrowedAmount * 2; - - uint256 wethTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - uint256 daiTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)); - - // (uint256 remainingFunds,) = creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.LIQUIDATE_ACCOUNT, - // totalValue, - // LIQUIDATOR, - // FRIEND, - // wethTokenMask | daiTokenMask, - // 0, - // borrowedAmount, - // 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( - wethGateway.balanceOf(FRIEND), - (totalValue * (PERCENTAGE_FACTOR - liquidationDiscount)) / PERCENTAGE_FACTOR, - "Incorrect amount were paid to liqiudator friend address" - ); - } - - /// @dev [CM-19]: closeCreditAccount sends ETH for WETH creditManger to borrower - /// CASE: LIQUIDATION - /// Underlying token: DAI - function test_CM_19_close_dai_credit_account_sends_eth_to_liquidator() public { - /// CLOSURE CASE - (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); - creditManager.transferAccountOwnership(creditAccount, address(this)); - - // Transfer additional borrowedAmount. After that underluying token balance = 2 * borrowedAmount - tokenTestSuite.mint(Tokens.DAI, creditAccount, borrowedAmount); - - // 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)); - - // (uint256 remainingFunds,) = creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.LIQUIDATE_ACCOUNT, - // borrowedAmount, - // LIQUIDATOR, - // FRIEND, - // wethTokenMask | daiTokenMask, - // 0, - // borrowedAmount, - // true - // ); - - expectBalance(Tokens.WETH, creditAccount, 1); - - assertEq( - wethGateway.balanceOf(FRIEND), - WETH_EXCHANGE_AMOUNT - 1, - "Incorrect amount were paid to liqiudator friend address" - ); - } - - // - // MANAGE DEBT - // - - /// @dev [CM-20]: manageDebt correctly increases debt - function test_CM_20_manageDebt_correctly_increases_debt(uint128 amount) public { - (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate,, address creditAccount) = cms.openCreditAccount(1); - - tokenTestSuite.mint(Tokens.DAI, address(poolMock), amount); - - poolMock.setCumulative_RAY(cumulativeIndexLastUpdate * 2); - - uint256 expectedNewCulumativeIndex = - (2 * cumulativeIndexLastUpdate * (borrowedAmount + amount)) / (2 * borrowedAmount + amount); - - (uint256 newBorrowedAmount,,) = - creditManager.manageDebt(creditAccount, amount, 1, ManageDebtAction.INCREASE_DEBT); - - assertEq(newBorrowedAmount, borrowedAmount + amount, "Incorrect returned newBorrowedAmount"); - - // assertLe( - // (ICreditAccount(creditAccount).cumulativeIndexLastUpdate() * (10 ** 6)) / expectedNewCulumativeIndex, - // 10 ** 6, - // "Incorrect cumulative index" - // ); - - (uint256 debt,,,,,) = creditManager.creditAccountInfo(creditAccount); - assertEq(debt, newBorrowedAmount, "Incorrect borrowedAmount"); - - expectBalance(Tokens.DAI, creditAccount, newBorrowedAmount, "Incorrect balance on credit account"); - - assertEq(poolMock.lendAmount(), amount, "Incorrect lend amount"); - - assertEq(poolMock.lendAccount(), creditAccount, "Incorrect lend account"); - } - - /// @dev [CM-21]: manageDebt correctly decreases debt - function test_CM_21_manageDebt_correctly_decreases_debt(uint128 amount) public { - // tokenTestSuite.mint(Tokens.DAI, address(poolMock), (uint256(type(uint128).max) * 14) / 10); - - // (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow, address creditAccount) = - // cms.openCreditAccount((uint256(type(uint128).max) * 14) / 10); - - // (,, uint256 totalDebt) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // uint256 expectedInterestAndFees; - // uint256 expectedBorrowAmount; - // if (amount >= totalDebt - borrowedAmount) { - // expectedInterestAndFees = 0; - // expectedBorrowAmount = totalDebt - amount; - // } else { - // expectedInterestAndFees = totalDebt - borrowedAmount - amount; - // expectedBorrowAmount = borrowedAmount; - // } - - // (uint256 newBorrowedAmount,) = - // creditManager.manageDebt(creditAccount, amount, 1, ManageDebtAction.DECREASE_DEBT); - - // assertEq(newBorrowedAmount, expectedBorrowAmount, "Incorrect returned newBorrowedAmount"); - - // if (amount >= totalDebt - borrowedAmount) { - // (,, uint256 newTotalDebt) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // assertEq(newTotalDebt, newBorrowedAmount, "Incorrect new interest"); - // } else { - // (,, uint256 newTotalDebt) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // assertLt( - // (RAY * (newTotalDebt - newBorrowedAmount)) / expectedInterestAndFees - RAY, - // 10000, - // "Incorrect new interest" - // ); - // } - // uint256 cumulativeIndexLastUpdateAfter; - // { - // uint256 debt; - // (debt, cumulativeIndexLastUpdateAfter,,,,) = creditManager.creditAccountInfo(creditAccount); - - // assertEq(debt, newBorrowedAmount, "Incorrect borrowedAmount"); - // } - - // expectBalance(Tokens.DAI, creditAccount, borrowedAmount - amount, "Incorrect balance on credit account"); - - // if (amount >= totalDebt - borrowedAmount) { - // assertEq(cumulativeIndexLastUpdateAfter, cumulativeIndexNow, "Incorrect cumulativeIndexLastUpdate"); - // } else { - // CreditManagerTestInternal cmi = new CreditManagerTestInternal( - // creditManager.poolService(), address(withdrawalManager) - // ); - - // { - // (uint256 feeInterest,,,,) = creditManager.fees(); - // amount = uint128((uint256(amount) * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR + feeInterest)); - // } - - // assertEq( - // cumulativeIndexLastUpdateAfter, - // cmi.calcNewCumulativeIndex(borrowedAmount, amount, cumulativeIndexNow, cumulativeIndexLastUpdate, false), - // "Incorrect cumulativeIndexLastUpdate" - // ); - // } - } - - // - // ADD COLLATERAL - // - - /// @dev [CM-22]: add collateral transfers money and returns token mask - - function test_CM_22_add_collateral_transfers_money_and_returns_token_mask() public { - (,,, address creditAccount) = _openCreditAccount(); - - tokenTestSuite.mint(Tokens.WETH, FRIEND, WETH_EXCHANGE_AMOUNT); - tokenTestSuite.approve(Tokens.WETH, FRIEND, address(creditManager)); - - expectBalance(Tokens.WETH, creditAccount, 0, "Non-zero WETH balance"); - - expectTokenIsEnabled(creditAccount, Tokens.WETH, false); - - uint256 tokenMask = creditManager.addCollateral( - FRIEND, creditAccount, tokenTestSuite.addressOf(Tokens.WETH), WETH_EXCHANGE_AMOUNT - ); - - expectBalance(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT, "Non-zero WETH balance"); - - expectBalance(Tokens.WETH, FRIEND, 0, "Incorrect FRIEND balance"); - - assertEq( - tokenMask, - creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)), - "Incorrect return result" - ); - - // expectTokenIsEnabled(creditAccount, Tokens.WETH, true); - } - - // - // TRANSFER ACCOUNT OWNERSHIP - // - - /// @dev [CM-23]: transferAccountOwnership reverts if to equals 0 or creditAccount is linked with "to" address - - function test_CM_23_transferAccountOwnership_reverts_if_account_exists() public { - // _openCreditAccount(); - - // creditManager.openCreditAccount(1, FRIEND); - - // // Existing account case - // vm.expectRevert(UserAlreadyHasAccountException.selector); - // creditManager.transferAccountOwnership(FRIEND, USER); - } - - /// @dev [CM-24]: transferAccountOwnership changes creditAccounts map properly - - function test_CM_24_transferAccountOwnership_changes_creditAccounts_map_properly() public { - (,,, address creditAccount) = _openCreditAccount(); - - creditManager.transferAccountOwnership(creditAccount, FRIEND); - - // assertEq(creditManager.creditAccounts(USER), address(0), "From account wasn't deleted"); - - // assertEq(creditManager.creditAccounts(FRIEND), creditAccount, "To account isn't correct"); - - // vm.expectRevert(CreditAccountNotExistsException.selector); - // creditManager.getCreditAccountOrRevert(USER); - } - - // - // APPROVE CREDIT ACCOUNT - // - - /// @dev [CM-25A]: approveCreditAccount reverts if the token is not added - function test_CM_25A_approveCreditAccount_reverts_if_the_token_is_not_added() public { - (,,, address creditAccount) = _openCreditAccount(); - creditManager.setCreditAccountForExternalCall(creditAccount); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - vm.expectRevert(TokenNotAllowedException.selector); - - vm.prank(ADAPTER); - creditManager.approveCreditAccount(DUMB_ADDRESS, 100); - } - - /// @dev [CM-26]: approveCreditAccount approves with desired allowance - function test_CM_26_approveCreditAccount_approves_with_desired_allowance() public { - (,,, address creditAccount) = _openCreditAccount(); - creditManager.setCreditAccountForExternalCall(creditAccount); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - // Case, when current allowance > Allowance_THRESHOLD - tokenTestSuite.approve(Tokens.DAI, creditAccount, DUMB_ADDRESS, 200); - - address dai = tokenTestSuite.addressOf(Tokens.DAI); - - vm.prank(ADAPTER); - creditManager.approveCreditAccount(dai, DAI_EXCHANGE_AMOUNT); - - expectAllowance(Tokens.DAI, creditAccount, DUMB_ADDRESS, DAI_EXCHANGE_AMOUNT); - } - - /// @dev [CM-27A]: approveCreditAccount works for ERC20 that revert if allowance > 0 before approve - function test_CM_27A_approveCreditAccount_works_for_ERC20_with_approve_restrictions() public { - (,,, address creditAccount) = _openCreditAccount(); - creditManager.setCreditAccountForExternalCall(creditAccount); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - address approveRevertToken = address(new ERC20ApproveRestrictedRevert()); - - vm.prank(CONFIGURATOR); - creditManager.addToken(approveRevertToken); - - vm.prank(ADAPTER); - creditManager.approveCreditAccount(approveRevertToken, DAI_EXCHANGE_AMOUNT); - - vm.prank(ADAPTER); - creditManager.approveCreditAccount(approveRevertToken, 2 * DAI_EXCHANGE_AMOUNT); - - expectAllowance(approveRevertToken, creditAccount, DUMB_ADDRESS, 2 * DAI_EXCHANGE_AMOUNT); - } - - // /// @dev [CM-27B]: approveCreditAccount works for ERC20 that returns false if allowance > 0 before approve - function test_CM_27B_approveCreditAccount_works_for_ERC20_with_approve_restrictions() public { - (,,, address creditAccount) = _openCreditAccount(); - creditManager.setCreditAccountForExternalCall(creditAccount); - - address approveFalseToken = address(new ERC20ApproveRestrictedFalse()); - - vm.prank(CONFIGURATOR); - creditManager.addToken(approveFalseToken); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - vm.prank(ADAPTER); - creditManager.approveCreditAccount(approveFalseToken, DAI_EXCHANGE_AMOUNT); - - vm.prank(ADAPTER); - creditManager.approveCreditAccount(approveFalseToken, 2 * DAI_EXCHANGE_AMOUNT); - - expectAllowance(approveFalseToken, creditAccount, DUMB_ADDRESS, 2 * DAI_EXCHANGE_AMOUNT); - } - - // - // EXECUTE ORDER - // - - /// @dev [CM-29]: executeOrder calls credit account method and emit event - function test_CM_29_executeOrder_calls_credit_account_method_and_emit_event() public { - (,,, address creditAccount) = _openCreditAccount(); - creditManager.setCreditAccountForExternalCall(creditAccount); - - TargetContractMock targetMock = new TargetContractMock(); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, address(targetMock)); - - bytes memory callData = bytes("Hello, world!"); - - // we emit the event we expect to see. - vm.expectEmit(true, false, false, false); - emit ExecuteOrder(address(targetMock)); - - // stack trace check - vm.expectCall(creditAccount, abi.encodeWithSignature("execute(address,bytes)", address(targetMock), callData)); - vm.expectCall(address(targetMock), callData); - - vm.prank(ADAPTER); - creditManager.executeOrder(callData); - - assertEq0(targetMock.callData(), callData, "Incorrect calldata"); - } - - // - // FULL COLLATERAL CHECK - // - - /// @dev [CM-38]: fullCollateralCheck skips tokens is they are not enabled - function test_CM_38_fullCollateralCheck_skips_tokens_is_they_are_not_enabled() public { - address creditAccount = _openAccountAndTransferToCF(); - - tokenTestSuite.mint(Tokens.USDC, creditAccount, USDC_ACCOUNT_AMOUNT); - - vm.expectRevert(NotEnoughCollateralException.selector); - _baseFullCollateralCheck(creditAccount); - - // fullCollateralCheck doesn't revert when token is enabled - uint256 usdcTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)); - uint256 daiTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)); - - creditManager.fullCollateralCheck(creditAccount, usdcTokenMask | daiTokenMask, new uint256[](0), 10000); - } - - /// @dev [CM-39]: fullCollateralCheck diables tokens if they have zero balance - function test_CM_39_fullCollateralCheck_diables_tokens_if_they_have_zero_balance() public { - (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow, address creditAccount) = - _openCreditAccount(); - creditManager.transferAccountOwnership(creditAccount, address(this)); - - (uint256 feeInterest,,,,) = creditManager.fees(); - - /// TODO: CHANGE COMPUTATION - - uint256 borrowAmountWithInterest = borrowedAmount * cumulativeIndexNow / cumulativeIndexLastUpdate; - uint256 interestAccured = borrowAmountWithInterest - borrowedAmount; - - uint256 amountToRepayInLINK = ( - ((borrowAmountWithInterest + interestAccured * feeInterest / PERCENTAGE_FACTOR) * (10 ** 8)) - * PERCENTAGE_FACTOR / tokenTestSuite.prices(Tokens.LINK) - / creditManager.liquidationThresholds(tokenTestSuite.addressOf(Tokens.LINK)) - ) + 1000000000; - - tokenTestSuite.mint(Tokens.LINK, creditAccount, amountToRepayInLINK); - - uint256 wethTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - uint256 daiTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)); - uint256 linkTokenMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - // Enable WETH and LINK token. WETH should be disabled adter fullCollateralCheck - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.LINK)); - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.WETH)); - - creditManager.fullCollateralCheck( - creditAccount, wethTokenMask | linkTokenMask | daiTokenMask, new uint256[](0), 10000 - ); - - expectTokenIsEnabled(creditAccount, Tokens.LINK, true); - expectTokenIsEnabled(creditAccount, Tokens.WETH, false); - } - - /// @dev [CM-40]: fullCollateralCheck breaks loop if total >= borrowAmountPlusInterestRateUSD and pass the check - function test_CM_40_fullCollateralCheck_breaks_loop_if_total_gte_borrowAmountPlusInterestRateUSD_and_pass_the_check( - ) public { - vm.startPrank(CONFIGURATOR); - - CreditManagerV3 cm = new CreditManagerV3(address(poolMock), address(withdrawalManager)); - cms.cr().addCreditManager(address(cm)); - - cm.setCreditFacade(address(this)); - cm.setPriceOracle(address(priceOracle)); - - vm.stopPrank(); - - address creditAccount = cm.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER, false); - cm.transferAccountOwnership(creditAccount, address(this)); - - address revertToken = DUMB_ADDRESS; - address linkToken = tokenTestSuite.addressOf(Tokens.LINK); - - // We add "revert" token - DUMB address which would revert if balanceOf method would be called - // If (total >= borrowAmountPlusInterestRateUSD) doesn't break the loop, it would be called - // cause we enable this token using checkAndEnableToken. - // If fullCollateralCheck doesn't revert, it means that the break works - vm.startPrank(CONFIGURATOR); - - cm.addToken(linkToken); - cm.addToken(revertToken); - cm.setCollateralTokenData( - linkToken, creditConfig.lt(Tokens.LINK), creditConfig.lt(Tokens.LINK), type(uint40).max, 0 - ); - - vm.stopPrank(); - - // cm.checkAndEnableToken(revertToken); - // cm.checkAndEnableToken(linkToken); - - // We add WAD for rounding compensation - uint256 amountToRepayInLINK = ((DAI_ACCOUNT_AMOUNT + WAD) * PERCENTAGE_FACTOR * (10 ** 8)) - / creditConfig.lt(Tokens.LINK) / tokenTestSuite.prices(Tokens.LINK); - - tokenTestSuite.mint(Tokens.LINK, creditAccount, amountToRepayInLINK); - - uint256 revertTokenMask = cm.getTokenMaskOrRevert(revertToken); - uint256 linkTokenMask = cm.getTokenMaskOrRevert(linkToken); - - uint256 enabledTokensMap = revertTokenMask | linkTokenMask; - - cm.fullCollateralCheck(creditAccount, enabledTokensMap, new uint256[](0), 10000); - } - - /// @dev [CM-41]: fullCollateralCheck reverts if CA has more than allowed enabled tokens - function test_CM_41_fullCollateralCheck_reverts_if_CA_has_more_than_allowed_enabled_tokens() public { - vm.startPrank(CONFIGURATOR); - - // We use clean CreditManagerV3 to have only one underlying token for testing - creditManager = new CreditManagerV3(address(poolMock), address(withdrawalManager)); - cms.cr().addCreditManager(address(creditManager)); - - creditManager.setCreditFacade(address(this)); - creditManager.setPriceOracle(address(priceOracle)); - - creditManager.setCollateralTokenData(poolMock.underlyingToken(), 9300, 9300, type(uint40).max, 0); - vm.stopPrank(); - - address creditAccount = creditManager.openCreditAccount(DAI_ACCOUNT_AMOUNT, address(this), false); - tokenTestSuite.mint(Tokens.DAI, creditAccount, 2 * DAI_ACCOUNT_AMOUNT); - - enableTokensMoreThanLimit(creditAccount); - vm.expectRevert(TooManyEnabledTokensException.selector); - - creditManager.fullCollateralCheck(creditAccount, 2 ** 13 - 1, new uint256[](0), 10000); - } - - /// @dev [CM-41A]: fullCollateralCheck correctly disables the underlying when needed - function test_CM_41A_fullCollateralCheck_correctly_dfisables_the_underlying_when_needed() public { - (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow, address creditAccount) = - _openCreditAccount(); - - uint256 daiBalance = tokenTestSuite.balanceOf(Tokens.DAI, creditAccount); - - tokenTestSuite.burn(Tokens.DAI, creditAccount, daiBalance); - - _addAndEnableTokens(creditAccount, 200, 0); - - uint256 totalTokens = creditManager.collateralTokensCount(); - - uint256 borrowAmountWithInterest = borrowedAmount * cumulativeIndexNow / cumulativeIndexLastUpdate; - uint256 interestAccured = borrowAmountWithInterest - borrowedAmount; - - (uint256 feeInterest,,,,) = creditManager.fees(); - - uint256 amountToRepayInLINK = ( - ((borrowAmountWithInterest + interestAccured * feeInterest / PERCENTAGE_FACTOR) * (10 ** 8)) - * PERCENTAGE_FACTOR / tokenTestSuite.prices(Tokens.DAI) - / creditManager.liquidationThresholds(tokenTestSuite.addressOf(Tokens.DAI)) - ) + WAD; - - tokenTestSuite.mint(Tokens.DAI, creditAccount, amountToRepayInLINK); - - uint256[] memory hints = new uint256[](totalTokens); - unchecked { - for (uint256 i; i < totalTokens; ++i) { - hints[i] = 2 ** (totalTokens - i - 1); - } - } - - creditManager.fullCollateralCheck(creditAccount, 2 ** (totalTokens) - 1, hints, 10000); - - assertEq( - creditManager.enabledTokensMaskOf(creditAccount).calcEnabledTokens(), - 1, - "Incorrect number of tokens enabled" - ); - } - - /// @dev [CM-42]: fullCollateralCheck fuzzing test - function test_CM_42_fullCollateralCheck_fuzzing_test( - uint128 borrowedAmount, - uint128 daiBalance, - uint128 usdcBalance, - uint128 linkBalance, - uint128 wethBalance, - bool enableUSDC, - bool enableLINK, - bool enableWETH, - uint16 minHealthFactor - ) public { - // vm.assume(borrowedAmount > WAD); - - // vm.assume(minHealthFactor > 10_000 && minHealthFactor < 50_000); - - // tokenTestSuite.mint(Tokens.DAI, address(poolMock), borrowedAmount); - - // (,,, address creditAccount) = cms.openCreditAccount(borrowedAmount); - // creditManager.transferAccountOwnership(creditAccount, address(this)); - - // if (daiBalance > borrowedAmount) { - // tokenTestSuite.mint(Tokens.DAI, creditAccount, daiBalance - borrowedAmount); - // } else { - // tokenTestSuite.burn(Tokens.DAI, creditAccount, borrowedAmount - daiBalance); - // } - - // expectBalance(Tokens.DAI, creditAccount, daiBalance); - - // mintBalance(creditAccount, Tokens.USDC, usdcBalance, enableUSDC); - // mintBalance(creditAccount, Tokens.LINK, linkBalance, enableLINK); - // mintBalance(creditAccount, Tokens.WETH, wethBalance, enableWETH); - - // uint256 twvUSD = ( - // tokenTestSuite.balanceOf(Tokens.DAI, creditAccount) * tokenTestSuite.prices(Tokens.DAI) - // * creditConfig.lt(Tokens.DAI) - // ) / WAD; - - // twvUSD += !enableUSDC - // ? 0 - // : ( - // tokenTestSuite.balanceOf(Tokens.USDC, creditAccount) * tokenTestSuite.prices(Tokens.USDC) - // * creditConfig.lt(Tokens.USDC) - // ) / (10 ** 6); - - // twvUSD += !enableLINK - // ? 0 - // : ( - // tokenTestSuite.balanceOf(Tokens.LINK, creditAccount) * tokenTestSuite.prices(Tokens.LINK) - // * creditConfig.lt(Tokens.LINK) - // ) / WAD; - - // twvUSD += !enableWETH - // ? 0 - // : ( - // tokenTestSuite.balanceOf(Tokens.WETH, creditAccount) * tokenTestSuite.prices(Tokens.WETH) - // * creditConfig.lt(Tokens.WETH) - // ) / WAD; - - // (,, uint256 borrowedAmountWithInterestAndFees) = creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // uint256 debtUSD = borrowedAmountWithInterestAndFees * tokenTestSuite.prices(Tokens.DAI) * minHealthFactor / WAD; - - // bool shouldRevert = twvUSD < debtUSD; - - // uint256 enabledTokensMap = 1; - - // if (enableUSDC) { - // enabledTokensMap |= creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)); - // } - - // if (enableLINK) { - // enabledTokensMap |= creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - // } - - // if (enableWETH) { - // enabledTokensMap |= creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - // } - - // if (shouldRevert) { - // vm.expectRevert(NotEnoughCollateralException.selector); - // } - - // creditManager.fullCollateralCheck(creditAccount, enabledTokensMap, new uint256[](0), minHealthFactor); - } - - // - // CALC CLOSE PAYMENT PURE - // - struct CalcClosePaymentsPureTestCase { - string name; - uint256 totalValue; - ClosureAction closureActionType; - uint256 borrowedAmount; - uint256 borrowedAmountWithInterest; - uint256 amountToPool; - uint256 remainingFunds; - uint256 profit; - uint256 loss; - } - - /// @dev [CM-43]: calcClosePayments computes - function test_CM_43_calcClosePayments_test() public { - // vm.prank(CONFIGURATOR); - - // creditManager.setParams( - // 1000, // feeInterest: 10% , it doesn't matter this test - // 200, // feeLiquidation: 2%, it doesn't matter this test - // 9500, // liquidationPremium: 5%, it doesn't matter this test - // 100, // feeLiquidationExpired: 1% - // 9800 // liquidationPremiumExpired: 2% - // ); - - // CalcClosePaymentsPureTestCase[7] memory cases = [ - // CalcClosePaymentsPureTestCase({ - // name: "CLOSURE", - // totalValue: 0, - // closureActionType: ClosureAction.CLOSE_ACCOUNT, - // borrowedAmount: 1000, - // borrowedAmountWithInterest: 1100, - // amountToPool: 1110, // amountToPool = 1100 + 100 * 10% = 1110 - // remainingFunds: 0, - // profit: 10, // profit: 100 (interest) * 10% = 10 - // loss: 0 - // }), - // CalcClosePaymentsPureTestCase({ - // name: "LIQUIDATION WITH PROFIT & REMAINING FUNDS", - // totalValue: 2000, - // closureActionType: ClosureAction.LIQUIDATE_ACCOUNT, - // borrowedAmount: 1000, - // borrowedAmountWithInterest: 1100, - // amountToPool: 1150, // amountToPool = 1100 + 100 * 10% + 2000 * 2% = 1150 - // remainingFunds: 749, //remainingFunds: 2000 * (100% - 5%) - 1150 - 1 = 749 - // profit: 50, - // loss: 0 - // }), - // CalcClosePaymentsPureTestCase({ - // name: "LIQUIDATION WITH PROFIT & ZERO REMAINING FUNDS", - // totalValue: 2100, - // closureActionType: ClosureAction.LIQUIDATE_ACCOUNT, - // borrowedAmount: 900, - // borrowedAmountWithInterest: 1900, - // amountToPool: 1995, // amountToPool = 1900 + 1000 * 10% + 2100 * 2% = 2042, totalFunds = 2100 * 95% = 1995, so, amount to pool would be 1995 - // remainingFunds: 0, // remainingFunds: 2000 * (100% - 5%) - 1150 - 1 = 749 - // profit: 95, - // loss: 0 - // }), - // CalcClosePaymentsPureTestCase({ - // name: "LIQUIDATION WITH LOSS", - // totalValue: 1000, - // closureActionType: ClosureAction.LIQUIDATE_ACCOUNT, - // borrowedAmount: 900, - // borrowedAmountWithInterest: 1900, - // amountToPool: 950, // amountToPool = 1900 + 1000 * 10% + 1000 * 2% = 2020, totalFunds = 1000 * 95% = 950, So, amount to pool would be 950 - // remainingFunds: 0, // 0, cause it's loss - // profit: 0, - // loss: 950 - // }), - // CalcClosePaymentsPureTestCase({ - // name: "LIQUIDATION OF EXPIRED WITH PROFIT & REMAINING FUNDS", - // totalValue: 2000, - // closureActionType: ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT, - // borrowedAmount: 1000, - // borrowedAmountWithInterest: 1100, - // amountToPool: 1130, // amountToPool = 1100 + 100 * 10% + 2000 * 1% = 1130 - // remainingFunds: 829, //remainingFunds: 2000 * (100% - 2%) - 1130 - 1 = 829 - // profit: 30, - // loss: 0 - // }), - // CalcClosePaymentsPureTestCase({ - // name: "LIQUIDATION OF EXPIRED WITH PROFIT & ZERO REMAINING FUNDS", - // totalValue: 2100, - // closureActionType: ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT, - // borrowedAmount: 900, - // borrowedAmountWithInterest: 2000, - // amountToPool: 2058, // amountToPool = 2000 + 1100 * 10% + 2100 * 1% = 2131, totalFunds = 2100 * 98% = 2058, so, amount to pool would be 2058 - // remainingFunds: 0, - // profit: 58, - // loss: 0 - // }), - // CalcClosePaymentsPureTestCase({ - // name: "LIQUIDATION OF EXPIRED WITH LOSS", - // totalValue: 1000, - // closureActionType: ClosureAction.LIQUIDATE_EXPIRED_ACCOUNT, - // borrowedAmount: 900, - // borrowedAmountWithInterest: 1900, - // amountToPool: 980, // amountToPool = 1900 + 1000 * 10% + 1000 * 2% = 2020, totalFunds = 1000 * 98% = 980, So, amount to pool would be 980 - // remainingFunds: 0, // 0, cause it's loss - // profit: 0, - // loss: 920 - // }) - // // CalcClosePaymentsPureTestCase({ - // // name: "LIQUIDATION WHILE PAUSED WITH REMAINING FUNDS", - // // totalValue: 2000, - // // closureActionType: ClosureAction.LIQUIDATE_PAUSED, - // // borrowedAmount: 1000, - // // borrowedAmountWithInterest: 1100, - // // amountToPool: 1150, // amountToPool = 1100 + 100 * 10% + 2000 * 2% = 1150 - // // remainingFunds: 849, //remainingFunds: 2000 - 1150 - 1 = 869 - // // profit: 50, - // // loss: 0 - // // }), - // // CalcClosePaymentsPureTestCase({ - // // name: "LIQUIDATION OF EXPIRED WITH LOSS", - // // totalValue: 1000, - // // closureActionType: ClosureAction.LIQUIDATE_PAUSED, - // // borrowedAmount: 900, - // // borrowedAmountWithInterest: 1900, - // // amountToPool: 1000, // amountToPool = 1900 + 1000 * 10% + 1000 * 2% = 2020, totalFunds = 1000 * 98% = 980, So, amount to pool would be 980 - // // remainingFunds: 0, // 0, cause it's loss - // // profit: 0, - // // loss: 900 - // // }) - // ]; - - // for (uint256 i = 0; i < cases.length; i++) { - // (uint256 amountToPool, uint256 remainingFunds, uint256 profit, uint256 loss) = creditManager - // .calcClosePayments( - // cases[i].totalValue, - // cases[i].closureActionType, - // cases[i].borrowedAmount, - // cases[i].borrowedAmountWithInterest - // ); - - // assertEq(amountToPool, cases[i].amountToPool, string(abi.encodePacked(cases[i].name, ": amountToPool"))); - // assertEq( - // remainingFunds, cases[i].remainingFunds, string(abi.encodePacked(cases[i].name, ": remainingFunds")) - // ); - // assertEq(profit, cases[i].profit, string(abi.encodePacked(cases[i].name, ": profit"))); - // assertEq(loss, cases[i].loss, string(abi.encodePacked(cases[i].name, ": loss"))); - // } - } - - // - // TRASNFER ASSETS TO - // - - /// @dev [CM-44]: _transferAssetsTo sends all tokens except underlying one and not-enabled to provided address - function test_CM_44_transferAssetsTo_sends_all_tokens_except_underlying_one_to_provided_address() public { - // It enables CreditManagerTestInternal for some test cases - _connectCreditManagerSuite(Tokens.DAI, true); - - address[2] memory friends = [FRIEND, FRIEND2]; - - // CASE 0: convertToETH = false - // CASE 1: convertToETH = true - for (uint256 i = 0; i < 2; i++) { - bool convertToETH = i > 0; - - address friend = friends[i]; - (uint256 borrowedAmount,,, address creditAccount) = _openCreditAccount(); - creditManager.transferAccountOwnership(creditAccount, address(this)); - - CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); - - tokenTestSuite.mint(Tokens.USDC, creditAccount, USDC_EXCHANGE_AMOUNT); - tokenTestSuite.mint(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); - tokenTestSuite.mint(Tokens.LINK, creditAccount, LINK_EXCHANGE_AMOUNT); - - address wethTokenAddr = tokenTestSuite.addressOf(Tokens.WETH); - // creditManager.checkAndEnableToken(wethTokenAddr); - - uint256 enabledTokensMask = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)) - | creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.WETH)); - - cmi.transferAssetsTo(creditAccount, friend, convertToETH, enabledTokensMask); - - expectBalance(Tokens.DAI, creditAccount, borrowedAmount, "Underlying assets were transffered!"); - - expectBalance(Tokens.DAI, friend, 0); - - expectBalance(Tokens.USDC, creditAccount, USDC_EXCHANGE_AMOUNT); - - expectBalance(Tokens.USDC, friend, 0); - - expectBalance(Tokens.WETH, creditAccount, 1); - - if (convertToETH) { - assertEq( - wethGateway.balanceOf(friend), - WETH_EXCHANGE_AMOUNT - 1, - "Incorrect amount were sent to friend address" - ); - } else { - expectBalance(Tokens.WETH, friend, WETH_EXCHANGE_AMOUNT - 1); - } - - expectBalance(Tokens.LINK, creditAccount, LINK_EXCHANGE_AMOUNT); - - expectBalance(Tokens.LINK, friend, 0); - - creditManager.transferAccountOwnership(creditAccount, USER); - // creditManager.closeCreditAccount( - // creditAccount, - // ClosureAction.LIQUIDATE_ACCOUNT, - // 0, - // LIQUIDATOR, - // friend, - // enabledTokensMask, - // 0, - // DAI_ACCOUNT_AMOUNT, - // false - // ); - } - } - - // - // SAFE TOKEN TRANSFER - // - - /// @dev [CM-45]: _safeTokenTransfer transfers tokens - function test_CM_45_safeTokenTransfer_transfers_tokens() public { - // It enables CreditManagerTestInternal for some test cases - _connectCreditManagerSuite(Tokens.DAI, true); - - uint256 WETH_TRANSFER = WETH_EXCHANGE_AMOUNT / 4; - - address[2] memory friends = [FRIEND, FRIEND2]; - - // CASE 0: convertToETH = false - // CASE 1: convertToETH = true - for (uint256 i = 0; i < 2; i++) { - bool convertToETH = i > 0; - - address friend = friends[i]; - (,,, address creditAccount) = _openCreditAccount(); - - CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); - - tokenTestSuite.mint(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT); - - cmi.safeTokenTransfer( - creditAccount, tokenTestSuite.addressOf(Tokens.WETH), friend, WETH_TRANSFER, convertToETH - ); - - expectBalance(Tokens.WETH, creditAccount, WETH_EXCHANGE_AMOUNT - WETH_TRANSFER); - - if (convertToETH) { - assertEq(wethGateway.balanceOf(friend), WETH_TRANSFER, "Incorrect amount were sent to friend address"); - } else { - expectBalance(Tokens.WETH, friend, WETH_TRANSFER); - } - - // creditManager.closeCreditAccount( - // creditAccount, ClosureAction.LIQUIDATE_ACCOUNT, 0, LIQUIDATOR, friend, 1, 0, DAI_ACCOUNT_AMOUNT, false - // ); - } - } - - // - // DISABLE TOKEN - // - - // /// @dev [CM-46]: _disableToken disabale tokens and do not enable it if called twice - // function test_CM_46__disableToken_disabale_tokens_and_do_not_enable_it_if_called_twice() public { - // // It enables CreditManagerTestInternal for some test cases - // _connectCreditManagerSuite(Tokens.DAI, true); - - // address creditAccount = _openAccountAndTransferToCF(); - - // address usdcToken = tokenTestSuite.addressOf(Tokens.USDC); - // // creditManager.checkAndEnableToken(usdcToken); - - // expectTokenIsEnabled(creditAccount, Tokens.USDC, true); - - // CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); - - // cmi.disableToken(usdcToken); - // expectTokenIsEnabled(creditAccount, Tokens.USDC, false); - - // cmi.disableToken(usdcToken); - // expectTokenIsEnabled(creditAccount, Tokens.USDC, false); - // } - - /// @dev [CM-47]: collateralTokens works as expected - function test_CM_47_collateralTokens_works_as_expected(address newToken, uint16 newLT) public { - // vm.assume(newToken != underlying && newToken != address(0)); - - // vm.startPrank(CONFIGURATOR); - - // // reset connected tokens - // CreditManagerV3 cm = new CreditManagerV3(address(poolMock), address(0)); - - // cm.setLiquidationThreshold(underlying, 9200); - - // (address token, uint16 lt) = cm.collateralTokens(0); - // assertEq(token, underlying, "incorrect underlying token"); - // assertEq(lt, 9200, "incorrect lt for underlying token"); - - // uint16 ltAlt = cm.liquidationThresholds(underlying); - // assertEq(ltAlt, 9200, "incorrect lt for underlying token"); - - // assertEq(cm.collateralTokensCount(), 1, "Incorrect length"); - - // cm.addToken(newToken); - // assertEq(cm.collateralTokensCount(), 2, "Incorrect length"); - // (token, lt) = cm.collateralTokens(1); - - // assertEq(token, newToken, "incorrect newToken token"); - // assertEq(lt, 0, "incorrect lt for newToken token"); - - // cm.setLiquidationThreshold(newToken, newLT); - // (token, lt) = cm.collateralTokens(1); - - // assertEq(token, newToken, "incorrect newToken token"); - // assertEq(lt, newLT, "incorrect lt for newToken token"); - - // ltAlt = cm.liquidationThresholds(newToken); - - // assertEq(ltAlt, newLT, "incorrect lt for newToken token"); - - // vm.stopPrank(); - } - - // - // GET CREDIT ACCOUNT OR REVERT - // - - /// @dev [CM-48]: getCreditAccountOrRevert reverts if borrower has no account - // function test_CM_48_getCreditAccountOrRevert_reverts_if_borrower_has_no_account() public { - // (,,, address creditAccount) = _openCreditAccount(); - - // assertEq(creditManager.getCreditAccountOrRevert(USER), creditAccount, "Incorrect credit account"); - - // vm.expectRevert(CreditAccountNotExistsException.selector); - // creditManager.getCreditAccountOrRevert(DUMB_ADDRESS); - // } - - // - // CALC CREDIT ACCOUNT ACCRUED INTEREST - // - - /// @dev [CM-49]: calcCreditAccountAccruedInterest computes correctly - function test_CM_49_calcCreditAccountAccruedInterest_computes_correctly(uint128 amount) public { - // tokenTestSuite.mint(Tokens.DAI, address(poolMock), amount); - // (,,, address creditAccount) = cms.openCreditAccount(amount); - - // uint256 expectedBorrowedAmount = amount; - - // (, uint256 cumulativeIndexLastUpdate,,,,) = creditManager.creditAccountInfo(creditAccount); - - // uint256 cumulativeIndexNow = poolMock._cumulativeIndex_RAY(); - // uint256 expectedBorrowedAmountWithInterest = - // (expectedBorrowedAmount * cumulativeIndexNow) / cumulativeIndexLastUpdate; - - // (uint256 feeInterest,,,,) = creditManager.fees(); - - // uint256 expectedFee = - // ((expectedBorrowedAmountWithInterest - expectedBorrowedAmount) * feeInterest) / PERCENTAGE_FACTOR; - - // (uint256 borrowedAmount, uint256 borrowedAmountWithInterest, uint256 borrowedAmountWithInterestAndFees) = - // creditManager.calcCreditAccountAccruedInterest(creditAccount); - - // assertEq(borrowedAmount, expectedBorrowedAmount, "Incorrect borrowed amount"); - // assertEq( - // borrowedAmountWithInterest, expectedBorrowedAmountWithInterest, "Incorrect borrowed amount with interest" - // ); - // assertEq( - // borrowedAmountWithInterestAndFees, - // expectedBorrowedAmountWithInterest + expectedFee, - // "Incorrect borrowed amount with interest and fees" - // ); - } - - // - // GET CREDIT ACCOUNT PARAMETERS - // - - /// @dev [CM-50]: getCreditAccountParameters return correct values - function test_CM_50_getCreditAccountParameters_return_correct_values() public { - // It enables CreditManagerTestInternal for some test cases - _connectCreditManagerSuite(Tokens.DAI, true); - - (,,, address creditAccount) = _openCreditAccount(); - - (uint256 expectedDebt, uint256 expectedcumulativeIndexLastUpdate,,,,) = - creditManager.creditAccountInfo(creditAccount); - - CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); - - (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate,) = cmi.getCreditAccountParameters(creditAccount); - - assertEq(borrowedAmount, expectedDebt, "Incorrect borrowed amount"); - assertEq(cumulativeIndexLastUpdate, expectedcumulativeIndexLastUpdate, "Incorrect cumulativeIndexLastUpdate"); - - assertEq(cumulativeIndexLastUpdate, expectedcumulativeIndexLastUpdate, "cumulativeIndexLastUpdate"); - } - - // - // SET PARAMS - // - - /// @dev [CM-51]: setParams sets configuration properly - function test_CM_51_setParams_sets_configuration_properly() public { - uint16 s_feeInterest = 8733; - uint16 s_feeLiquidation = 1233; - uint16 s_liquidationPremium = 1220; - uint16 s_feeLiquidationExpired = 1221; - uint16 s_liquidationPremiumExpired = 7777; - - vm.prank(CONFIGURATOR); - creditManager.setParams( - s_feeInterest, s_feeLiquidation, s_liquidationPremium, s_feeLiquidationExpired, s_liquidationPremiumExpired - ); - ( - uint16 feeInterest, - uint16 feeLiquidation, - uint16 liquidationDiscount, - uint16 feeLiquidationExpired, - uint16 liquidationPremiumExpired - ) = creditManager.fees(); - - assertEq(feeInterest, s_feeInterest, "Incorrect feeInterest"); - assertEq(feeLiquidation, s_feeLiquidation, "Incorrect feeLiquidation"); - assertEq(liquidationDiscount, s_liquidationPremium, "Incorrect liquidationDiscount"); - assertEq(feeLiquidationExpired, s_feeLiquidationExpired, "Incorrect feeLiquidationExpired"); - assertEq(liquidationPremiumExpired, s_liquidationPremiumExpired, "Incorrect liquidationPremiumExpired"); - } - - // - // ADD TOKEN - // - - /// @dev [CM-52]: addToken reverts if token exists and if collateralTokens > 256 - function test_CM_52_addToken_reverts_if_token_exists_and_if_collateralTokens_more_256() public { - vm.startPrank(CONFIGURATOR); - - vm.expectRevert(TokenAlreadyAddedException.selector); - creditManager.addToken(underlying); - - for (uint256 i = creditManager.collateralTokensCount(); i < 248; i++) { - creditManager.addToken(address(uint160(uint256(keccak256(abi.encodePacked(i)))))); - } - - vm.expectRevert(TooManyTokensException.selector); - creditManager.addToken(DUMB_ADDRESS); - - vm.stopPrank(); - } - - /// @dev [CM-53]: addToken adds token and set tokenMaskMap correctly - function test_CM_53_addToken_adds_token_and_set_tokenMaskMap_correctly() public { - uint256 count = creditManager.collateralTokensCount(); - - vm.prank(CONFIGURATOR); - creditManager.addToken(DUMB_ADDRESS); - - assertEq(creditManager.collateralTokensCount(), count + 1, "collateralTokensCount want incremented"); - - assertEq(creditManager.getTokenMaskOrRevert(DUMB_ADDRESS), 1 << count, "tokenMaskMap was set incorrectly"); - } - - // - // SET LIQUIDATION THRESHOLD - // - - /// @dev [CM-54]: setLiquidationThreshold reverts for unknown token - function test_CM_54_setLiquidationThreshold_reverts_for_unknown_token() public { - vm.prank(CONFIGURATOR); - vm.expectRevert(TokenNotAllowedException.selector); - creditManager.setCollateralTokenData(DUMB_ADDRESS, 8000, 8000, type(uint40).max, 0); - } - - // // - // // SET FORBID MASK - // // - // /// @dev [CM-55]: setForbidMask sets forbidMask correctly - // function test_CM_55_setForbidMask_sets_forbidMask_correctly() public { - // uint256 expectedForbidMask = 244; - - // assertTrue(creditManager.forbiddenTokenMask() != expectedForbidMask, "expectedForbidMask is already the same"); - - // vm.prank(CONFIGURATOR); - // creditManager.setForbidMask(expectedForbidMask); - - // assertEq(creditManager.forbiddenTokenMask(), expectedForbidMask, "ForbidMask is not set correctly"); - // } - - // - // CHANGE CONTRACT AllowanceAction - // - - /// @dev [CM-56]: setContractAllowance updates adapterToContract - function test_CM_56_setContractAllowance_updates_adapterToContract() public { - assertTrue( - creditManager.adapterToContract(ADAPTER) != DUMB_ADDRESS, "adapterToContract(ADAPTER) is already the same" - ); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - assertEq(creditManager.adapterToContract(ADAPTER), DUMB_ADDRESS, "adapterToContract is not set correctly"); - - assertEq(creditManager.contractToAdapter(DUMB_ADDRESS), ADAPTER, "adapterToContract is not set correctly"); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, address(0)); - - assertEq(creditManager.adapterToContract(ADAPTER), address(0), "adapterToContract is not set correctly"); - - assertEq(creditManager.contractToAdapter(address(0)), address(0), "adapterToContract is not set correctly"); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); - - vm.prank(CONFIGURATOR); - creditManager.setContractAllowance(address(0), DUMB_ADDRESS); - - assertEq(creditManager.adapterToContract(address(0)), address(0), "adapterToContract is not set correctly"); - - assertEq(creditManager.contractToAdapter(DUMB_ADDRESS), address(0), "adapterToContract is not set correctly"); - - // vm.prank(CONFIGURATOR); - // creditManager.setContractAllowance(ADAPTER, UNIVERSAL_CONTRACT); - - // assertEq(creditManager.universalAdapter(), ADAPTER, "Universal adapter is not correctly set"); - - // vm.prank(CONFIGURATOR); - // creditManager.setContractAllowance(address(0), UNIVERSAL_CONTRACT); - - // assertEq(creditManager.universalAdapter(), address(0), "Universal adapter is not correctly set"); - } - - // - // UPGRADE CONTRACTS - // - - /// @dev [CM-57A]: setCreditFacade updates Credit Facade correctly - function test_CM_57A_setCreditFacade_updates_contract_correctly() public { - assertTrue(creditManager.creditFacade() != DUMB_ADDRESS, "creditFacade( is already the same"); - - vm.prank(CONFIGURATOR); - creditManager.setCreditFacade(DUMB_ADDRESS); - - assertEq(creditManager.creditFacade(), DUMB_ADDRESS, "creditFacade is not set correctly"); - } - - /// @dev [CM-57B]: setPriceOracle updates contract correctly - function test_CM_57_setPriceOracle_updates_contract_correctly() public { - assertTrue(address(creditManager.priceOracle()) != DUMB_ADDRESS2, "priceOracle is already the same"); - - vm.prank(CONFIGURATOR); - creditManager.setPriceOracle(DUMB_ADDRESS2); - - assertEq(address(creditManager.priceOracle()), DUMB_ADDRESS2, "priceOracle is not set correctly"); - } - - // - // SET CONFIGURATOR - // - - /// @dev [CM-58]: setCreditConfigurator sets creditConfigurator correctly and emits event - function test_CM_58_setCreditConfigurator_sets_creditConfigurator_correctly_and_emits_event() public { - assertTrue(creditManager.creditConfigurator() != DUMB_ADDRESS, "creditConfigurator is already the same"); - - vm.prank(CONFIGURATOR); - - vm.expectEmit(true, false, false, false); - emit SetCreditConfigurator(DUMB_ADDRESS); - - creditManager.setCreditConfigurator(DUMB_ADDRESS); - - assertEq(creditManager.creditConfigurator(), DUMB_ADDRESS, "creditConfigurator is not set correctly"); - } - - // /// @dev [CM-59]: _getTokenIndexByAddress works properly - // function test_CM_59_getMaxIndex_works_properly(uint256 noise) public { - // CreditManagerTestInternal cm = new CreditManagerTestInternal( - // address(poolMock) - // ); - - // for (uint256 i = 0; i < 256; i++) { - // uint256 mask = 1 << i; - // if (mask > noise) mask |= noise; - // uint256 value = cm.getMaxIndex(mask); - // assertEq(i, value, "Incorrect result"); - // } - // } - - // /// @dev [CM-60]: CreditManagerV3 allows approveCreditAccount and executeOrder for universal adapter - // function test_CM_60_universal_adapter_can_call_adapter_restricted_functions() public { - // TargetContractMock targetMock = new TargetContractMock(); - - // vm.prank(CONFIGURATOR); - // creditManager.setContractAllowance(ADAPTER, UNIVERSAL_CONTRACT_ADDRESS); - - // _openAccountAndTransferToCF(); - - // vm.prank(ADAPTER); - // creditManager.approveCreditAccount(DUMB_ADDRESS, underlying, type(uint256).max); - - // bytes memory callData = bytes("Hello"); - - // vm.prank(ADAPTER); - // creditManager.executeOrder(address(targetMock), callData); - // } - - /// @dev [CM-61]: setMaxEnabledToken correctly sets value - function test_CM_61_setMaxEnabledTokens_works_correctly() public { - vm.prank(CONFIGURATOR); - creditManager.setMaxEnabledTokens(255); - - assertEq(creditManager.maxAllowedEnabledTokenLength(), 255, "Incorrect max enabled tokens"); - } - - // /// @dev [CM-64]: closeCreditAccount reverts when attempting to liquidate while paused, - // /// and the payer is not set as emergency liquidator - - // function test_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 [CM-65]: Emergency liquidator can't close an account instead of liquidating - - // function test_CM_65_closeCreditAccount_reverts_when_paused_and_liquidator_tries_to_close() public { - // 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); - // } - - /// @dev [CM-66]: calcNewCumulativeIndex works correctly for various values - function test_CM_66_calcNewCumulativeIndex_is_correct( - uint128 borrowedAmount, - uint256 indexAtOpen, - uint256 indexNow, - uint128 delta, - bool isIncrease - ) public { - // vm.assume(borrowedAmount > 100); - // vm.assume(uint256(borrowedAmount) + uint256(delta) <= 2 ** 128 - 1); - - // indexNow = indexNow < RAY ? indexNow + RAY : indexNow; - // indexAtOpen = indexAtOpen < RAY ? indexAtOpen + RAY : indexNow; - - // vm.assume(indexNow <= 100 * RAY); - // vm.assume(indexNow >= indexAtOpen); - // vm.assume(indexNow - indexAtOpen < 10 * RAY); - - // uint256 interest = uint256((borrowedAmount * indexNow) / indexAtOpen - borrowedAmount); - - // vm.assume(interest > 1); - - // if (!isIncrease && (delta > interest)) delta %= uint128(interest); - - // CreditManagerTestInternal cmi = new CreditManagerTestInternal( - // creditManager.poolService(), address(withdrawalManager) - // ); - - // if (isIncrease) { - // uint256 newIndex = CreditLogic.calcNewCumulativeIndex(borrowedAmount, delta, indexNow, indexAtOpen, true); - - // uint256 newInterestError = ((borrowedAmount + delta) * indexNow) / newIndex - (borrowedAmount + delta) - // - ((borrowedAmount * indexNow) / indexAtOpen - borrowedAmount); - - // uint256 newTotalDebt = ((borrowedAmount + delta) * indexNow) / newIndex; - - // assertLe((RAY * newInterestError) / newTotalDebt, 10000, "Interest error is larger than 10 ** -23"); - // } else { - // uint256 newIndex = cmi.calcNewCumulativeIndex(borrowedAmount, delta, indexNow, indexAtOpen, false); - - // uint256 newTotalDebt = ((borrowedAmount * indexNow) / newIndex); - // uint256 newInterestError = newTotalDebt - borrowedAmount - (interest - delta); - - // emit log_uint(indexNow); - // emit log_uint(indexAtOpen); - // emit log_uint(interest); - // emit log_uint(delta); - // emit log_uint(interest - delta); - // emit log_uint(newTotalDebt); - // emit log_uint(borrowedAmount); - // emit log_uint(newInterestError); - - // assertLe((RAY * newInterestError) / newTotalDebt, 10000, "Interest error is larger than 10 ** -23"); - // } - } - - // /// @dev [CM-67]: checkEmergencyPausable returns pause state and enable emergencyLiquidation if needed - // function test_CM_67_checkEmergencyPausable_returns_pause_state_and_enable_emergencyLiquidation_if_needed() public { - // bool p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, true); - // assertTrue(!p, "Incorrect paused() value for non-paused state"); - // assertTrue(!creditManager.emergencyLiquidation(), "Emergency liquidation true when expected false"); - - // vm.prank(CONFIGURATOR); - // creditManager.pause(); - - // p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, true); - // assertTrue(p, "Incorrect paused() value for paused state"); - // assertTrue(!creditManager.emergencyLiquidation(), "Emergency liquidation true when expected false"); - - // vm.prank(CONFIGURATOR); - // creditManager.unpause(); - - // vm.prank(CONFIGURATOR); - // creditManager.addEmergencyLiquidator(DUMB_ADDRESS); - // p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, true); - // assertTrue(!p, "Incorrect paused() value for non-paused state"); - // assertTrue(!creditManager.emergencyLiquidation(), "Emergency liquidation true when expected false"); - - // vm.prank(CONFIGURATOR); - // creditManager.pause(); - - // p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, true); - // assertTrue(p, "Incorrect paused() value for paused state"); - // assertTrue(creditManager.emergencyLiquidation(), "Emergency liquidation flase when expected true"); - - // p = creditManager.checkEmergencyPausable(DUMB_ADDRESS, false); - // assertTrue(p, "Incorrect paused() value for paused state"); - // assertTrue(!creditManager.emergencyLiquidation(), "Emergency liquidation true when expected false"); - // } - - /// @dev [CM-68]: fullCollateralCheck checks tokens in correct order - function test_CM_68_fullCollateralCheck_is_evaluated_in_order_of_hints() public { - (uint256 borrowedAmount, uint256 cumulativeIndexLastUpdate, uint256 cumulativeIndexNow, address creditAccount) = - _openCreditAccount(); - - uint256 daiBalance = tokenTestSuite.balanceOf(Tokens.DAI, creditAccount); - - tokenTestSuite.burn(Tokens.DAI, creditAccount, daiBalance); - - uint256 borrowAmountWithInterest = borrowedAmount * cumulativeIndexNow / cumulativeIndexLastUpdate; - uint256 interestAccured = borrowAmountWithInterest - borrowedAmount; - - (uint256 feeInterest,,,,) = creditManager.fees(); - - uint256 amountToRepay = ( - ((borrowAmountWithInterest + interestAccured * feeInterest / PERCENTAGE_FACTOR) * (10 ** 8)) - * PERCENTAGE_FACTOR / tokenTestSuite.prices(Tokens.DAI) - / creditManager.liquidationThresholds(tokenTestSuite.addressOf(Tokens.DAI)) - ) + WAD; - - tokenTestSuite.mint(Tokens.DAI, creditAccount, amountToRepay); - - tokenTestSuite.mint(Tokens.USDC, creditAccount, USDC_ACCOUNT_AMOUNT); - tokenTestSuite.mint(Tokens.USDT, creditAccount, 10); - tokenTestSuite.mint(Tokens.LINK, creditAccount, 10); - - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.USDC)); - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.USDT)); - // creditManager.checkAndEnableToken(tokenTestSuite.addressOf(Tokens.LINK)); - - uint256[] memory collateralHints = new uint256[](2); - collateralHints[0] = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDT)); - collateralHints[1] = creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - - vm.expectCall(tokenTestSuite.addressOf(Tokens.USDT), abi.encodeCall(IERC20.balanceOf, (creditAccount))); - vm.expectCall(tokenTestSuite.addressOf(Tokens.LINK), abi.encodeCall(IERC20.balanceOf, (creditAccount))); - - uint256 enabledTokensMap = 1 | creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDC)) - | creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.USDT)) - | creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.LINK)); - - creditManager.fullCollateralCheck(creditAccount, enabledTokensMap, collateralHints, PERCENTAGE_FACTOR); - - // assertEq(cmi.fullCheckOrder(0), tokenTestSuite.addressOf(Tokens.USDT), "Token order incorrect"); - - // assertEq(cmi.fullCheckOrder(1), tokenTestSuite.addressOf(Tokens.LINK), "Token order incorrect"); - - // assertEq(cmi.fullCheckOrder(2), tokenTestSuite.addressOf(Tokens.DAI), "Token order incorrect"); - - // assertEq(cmi.fullCheckOrder(3), tokenTestSuite.addressOf(Tokens.USDC), "Token order incorrect"); - } - - /// @dev [CM-70]: fullCollateralCheck reverts when an illegal mask is passed in collateralHints - function test_CM_70_fullCollateralCheck_reverts_for_illegal_mask_in_hints() public { - (,,, address creditAccount) = _openCreditAccount(); - - vm.expectRevert(TokenNotAllowedException.selector); - - uint256[] memory ch = new uint256[](1); - ch[0] = 3; - - uint256 enabledTokensMap = 1; - - creditManager.fullCollateralCheck(creditAccount, enabledTokensMap, ch, PERCENTAGE_FACTOR); - } - - /// @dev [CM-71]: rampLiquidationThreshold correctly updates the internal struct - function test_CM_71_rampLiquidationThreshold_correctly_updates_parameters() public { - _connectCreditManagerSuite(Tokens.DAI, true); - - address usdc = tokenTestSuite.addressOf(Tokens.USDC); - - CreditManagerTestInternal cmi = CreditManagerTestInternal(address(creditManager)); - - vm.prank(CONFIGURATOR); - cmi.setCollateralTokenData(usdc, 8500, 9000, uint40(block.timestamp), 3600 * 24 * 7); - - CollateralTokenData memory cd = cmi.collateralTokensDataExt(cmi.getTokenMaskOrRevert(usdc)); - - assertEq(uint256(cd.ltInitial), creditConfig.lt(Tokens.USDC), "Incorrect initial LT"); - - assertEq(uint256(cd.ltFinal), 8500, "Incorrect final LT"); - - assertEq(uint256(cd.timestampRampStart), block.timestamp, "Incorrect timestamp start"); - - assertEq(uint256(cd.rampDuration), 3600 * 24 * 7, "Incorrect ramp duration"); - } - - /// @dev [CM-72]: Ramping liquidation threshold fuzzing - function test_CM_72_liquidation_ramping_fuzzing( - uint16 initialLT, - uint16 newLT, - uint24 duration, - uint256 timestampCheck - ) public { - // initialLT = 1000 + (initialLT % (DEFAULT_UNDERLYING_LT - 999)); - // newLT = 1000 + (newLT % (DEFAULT_UNDERLYING_LT - 999)); - // duration = 3600 + (duration % (3600 * 24 * 90 - 3600)); - - // timestampCheck = block.timestamp + (timestampCheck % (duration + 1)); - - // address usdc = tokenTestSuite.addressOf(Tokens.USDC); - - // uint256 timestampStart = block.timestamp; - - // vm.startPrank(CONFIGURATOR); - // creditManager.setCollateralTokenData(usdc, initialLT); - // creditManager.rampLiquidationThreshold(usdc, newLT, uint40(block.timestamp), duration); - - // assertEq(creditManager.liquidationThresholds(usdc), initialLT, "LT at ramping start incorrect"); - - // uint16 expectedLT; - // if (newLT >= initialLT) { - // expectedLT = uint16( - // uint256(initialLT) - // + (uint256(newLT - initialLT) * (timestampCheck - timestampStart)) / uint256(duration) - // ); - // } else { - // expectedLT = uint16( - // uint256(initialLT) - // - (uint256(initialLT - newLT) * (timestampCheck - timestampStart)) / uint256(duration) - // ); - // } - - // vm.warp(timestampCheck); - // uint16 actualLT = creditManager.liquidationThresholds(usdc); - // uint16 diff = actualLT > expectedLT ? actualLT - expectedLT : expectedLT - actualLT; - - // assertLe(diff, 1, "LT off by more than 1"); - - // vm.warp(timestampStart + duration + 1); - - // assertEq(creditManager.liquidationThresholds(usdc), newLT, "LT at ramping end incorrect"); - } - - // /// @notice [UA-3]: UniversalAdapter `revokeAllowances` reverts if passed zero address - // function test_UA_03_revokeAllowances_reverts_if_passed_zero_address() public { - // _openTestCreditAccount(); - - // RevocationPair[] memory revocations = new RevocationPair[](1); - - // vm.prank(USER); - // revocations[0] = RevocationPair({spender: address(0), token: usdc}); - // vm.expectRevert(ZeroAddressException.selector); - // creditFacade.multicall( - // multicallBuilder( - // MultiCall({ - // target: address(universalAdapter), - // callData: abi.encodeCall(universalAdapter.revokeAdapterAllowances, (revocations)) - // }) - // ) - // ); - - // vm.prank(USER); - // revocations[0] = RevocationPair({spender: address(targetMock1), token: address(0)}); - // vm.expectRevert(ZeroAddressException.selector); - // creditFacade.multicall( - // multicallBuilder( - // MultiCall({ - // target: address(universalAdapter), - // callData: abi.encodeCall(universalAdapter.revokeAdapterAllowances, (revocations)) - // }) - // ) - // ); - // } - - // /// @notice [UA-4]: UniversalAdapter `revokeAllowances` works as expected - // function test_UA_04_revokeAllowances_works_as_expected() public { - // (address creditAccount,) = _openTestCreditAccount(); - - // vm.prank(USER); - // creditFacade.multicall( - // multicallBuilder( - // MultiCall({ - // target: address(adapterMock1), - // callData: abi.encodeCall(adapterMock1.approveToken, (usdc, 10)) - // }), - // MultiCall({ - // target: address(adapterMock1), - // callData: abi.encodeCall(adapterMock1.approveToken, (dai, 20)) - // }), - // MultiCall({ - // target: address(adapterMock2), - // callData: abi.encodeCall(adapterMock2.approveToken, (dai, 30)) - // }) - // ) - // ); - - // RevocationPair[] memory revocations = new RevocationPair[](3); - // revocations[0] = RevocationPair({spender: address(targetMock1), token: usdc}); - // revocations[1] = RevocationPair({spender: address(targetMock2), token: usdc}); - // revocations[2] = RevocationPair({spender: address(targetMock2), token: dai}); - - // vm.expectCall( - // address(creditManager), - // abi.encodeCall(ICreditManagerV3.approveCreditAccount, (address(targetMock1), usdc, 1)) - // ); - // vm.expectCall( - // address(creditManager), - // abi.encodeCall(ICreditManagerV3.approveCreditAccount, (address(targetMock2), dai, 1)) - // ); - - // vm.prank(USER); - // creditFacade.multicall( - // multicallBuilder( - // MultiCall({ - // target: address(universalAdapter), - // callData: abi.encodeCall(universalAdapter.revokeAdapterAllowances, (revocations)) - // }) - // ) - // ); - - // expectAllowance(usdc, creditAccount, address(targetMock1), 1); - // expectAllowance(dai, creditAccount, address(targetMock1), 20); - // expectAllowance(usdc, creditAccount, address(targetMock2), 0); - // expectAllowance(dai, creditAccount, address(targetMock2), 1); - // } - // } -} diff --git a/contracts/test/unit/credit/CreditManagerV3.unit.t.sol b/contracts/test/unit/credit/CreditManagerV3.unit.t.sol new file mode 100644 index 00000000..bcba9fb7 --- /dev/null +++ b/contracts/test/unit/credit/CreditManagerV3.unit.t.sol @@ -0,0 +1,2407 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +/// MOCKS +import "../../../interfaces/IAddressProviderV3.sol"; +import {AddressProviderV3ACLMock} from "../../mocks/core/AddressProviderV3ACLMock.sol"; +import {AccountFactoryMock} from "../../mocks/core/AccountFactoryMock.sol"; +import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; + +import {AccountFactory} from "@gearbox-protocol/core-v2/contracts/core/AccountFactory.sol"; +import {CreditManagerV3Harness} from "./CreditManagerV3Harness.sol"; +import {CreditManagerV3Harness_USDT} from "./CreditManagerV3Harness_USDT.sol"; +import "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; + +// LIBS & TRAITS +import {UNDERLYING_TOKEN_MASK, BitMask} from "../../../libraries/BitMask.sol"; +import {CreditLogic} from "../../../libraries/CreditLogic.sol"; +import {CollateralLogic} from "../../../libraries/CollateralLogic.sol"; +import {USDTFees} from "../../../libraries/USDTFees.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +/// INTERFACE +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {ENTERED} from "../../../traits/ReentrancyGuardTrait.sol"; +import {ICreditAccount} from "@gearbox-protocol/core-v2/contracts/interfaces/ICreditAccount.sol"; +import {IAccountFactory} from "../../../interfaces/IAccountFactory.sol"; +import { + ICreditManagerV3, + ClosureAction, + CollateralTokenData, + ManageDebtAction, + CreditAccountInfo, + RevocationPair, + CollateralDebtData, + CollateralCalcTask, + ICreditManagerV3Events, + WITHDRAWAL_FLAG +} from "../../../interfaces/ICreditManagerV3.sol"; +import {IWETH} from "@gearbox-protocol/core-v2/contracts/interfaces/external/IWETH.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import {IPriceOracleV2, IPriceOracleV2Ext} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; +import {IWETHGateway} from "../../../interfaces/IWETHGateway.sol"; +import {ClaimAction, IWithdrawalManager} from "../../../interfaces/IWithdrawalManager.sol"; +import {IPoolQuotaKeeper} from "../../../interfaces/IPoolQuotaKeeper.sol"; +import {IPoolService} from "@gearbox-protocol/core-v2/contracts/interfaces/IPoolService.sol"; + +import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; + +// EXCEPTIONS +import "../../../interfaces/IExceptions.sol"; + +// MOCKS +import {PriceOracleMock} from "../../mocks/oracles/PriceOracleMock.sol"; +import {PoolMock} from "../../mocks/pool/PoolMock.sol"; +import {PoolQuotaKeeperMock} from "../../mocks/pool/PoolQuotaKeeperMock.sol"; +import {ERC20FeeMock} from "../../mocks/token/ERC20FeeMock.sol"; +import {ERC20Mock} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/ERC20Mock.sol"; +import {WETHGatewayMock} from "../../mocks/support/WETHGatewayMock.sol"; +import {CreditAccountMock, CreditAccountMockEvents} from "../../mocks/credit/CreditAccountMock.sol"; +import {WithdrawalManagerMock} from "../../mocks/support/WithdrawalManagerMock.sol"; +// SUITES +import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; +import {Tokens} from "../../config/Tokens.sol"; +import {CreditConfig} from "../../config/CreditConfig.sol"; + +// EXCEPTIONS +import "../../../interfaces/IExceptions.sol"; + +// TESTS +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 { + using BitMask for uint256; + using CreditLogic for CollateralTokenData; + using CreditLogic for CollateralDebtData; + using CollateralLogic for CollateralDebtData; + using USDTFees for uint256; + using Vars for VarU256; + + IAddressProviderV3 addressProvider; + + AccountFactoryMock accountFactory; + CreditManagerV3Harness creditManager; + PoolMock poolMock; + PoolQuotaKeeperMock poolQuotaKeeperMock; + + PriceOracleMock priceOracleMock; + WETHGatewayMock wethGateway; + WithdrawalManagerMock withdrawalManager; + + address underlying; + bool supportsQuotas; + + CreditConfig creditConfig; + + // Fee token settings + bool isFeeToken; + uint256 tokenFee = 0; + uint256 maxTokenFee = 0; + + /// @notice deploy credit manager without quotas support + modifier withoutSupportQuotas() { + _deployCreditManager(false); + _; + } + + /// @notice deploy credit manager with quotas support + modifier withSupportQuotas() { + _deployCreditManager(true); + _; + } + + /// @notice dexecute test twice with and without quotas support + modifier allQuotaCases() { + uint256 snapShot = vm.snapshot(); + _deployCreditManager(false); + _; + vm.revertTo(snapShot); + _deployCreditManager(true); + _; + } + + /// @notice execute test twice with normal and fee token as underlying + /// Should be before quota- modifiers + modifier withFeeTokenCase() { + uint256 snapshot = vm.snapshot(); + _setUnderlying({underlyingIsFeeToken: false}); + _; + + vm.revertTo(snapshot); + _setUnderlying({underlyingIsFeeToken: true}); + // set fee + _; + } + + function setUp() public { + tokenTestSuite = new TokensTestSuite(); + tokenTestSuite.topUpWETH{value: 100 * WAD}(); + + underlying = tokenTestSuite.addressOf(Tokens.DAI); + + addressProvider = new AddressProviderV3ACLMock(); + addressProvider.setAddress(AP_WETH_TOKEN, tokenTestSuite.addressOf(Tokens.WETH), false); + + accountFactory = AccountFactoryMock(addressProvider.getAddressOrRevert(AP_ACCOUNT_FACTORY, NO_VERSION_CONTROL)); + wethGateway = WETHGatewayMock(addressProvider.getAddressOrRevert(AP_WETH_GATEWAY, 3_00)); + withdrawalManager = WithdrawalManagerMock(addressProvider.getAddressOrRevert(AP_WITHDRAWAL_MANAGER, 3_00)); + + priceOracleMock = PriceOracleMock(addressProvider.getAddressOrRevert(AP_PRICE_ORACLE, 2)); + + /// Inits all state + supportsQuotas = false; + isFeeToken = false; + tokenFee = 0; + maxTokenFee = 0; + } + /// + /// HELPERS + /// + + function _deployCreditManager(bool _supportsQuotas) internal { + supportsQuotas = _supportsQuotas; + poolMock = new PoolMock(address(addressProvider), underlying); + poolMock.setSupportsQuotas(_supportsQuotas); + + if (_supportsQuotas) { + poolQuotaKeeperMock = new PoolQuotaKeeperMock(address(poolMock), underlying); + poolMock.setPoolQuotaKeeper(address(poolQuotaKeeperMock)); + caseName = string.concat(caseName, " [ supportsQuotas = true ] "); + } else { + caseName = string.concat(caseName, " [ supportsQuotas = false ] "); + } + + creditManager = (isFeeToken) + ? new CreditManagerV3Harness_USDT(address(addressProvider), address(poolMock)) + : new CreditManagerV3Harness(address(addressProvider), address(poolMock)); + creditManager.setCreditFacade(address(this)); + + creditManager.setFees( + DEFAULT_FEE_INTEREST, + DEFAULT_FEE_LIQUIDATION, + PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM, + DEFAULT_FEE_LIQUIDATION_EXPIRED, + PERCENTAGE_FACTOR - DEFAULT_LIQUIDATION_PREMIUM_EXPIRED + ); + + creditManager.setCollateralTokenData({ + token: underlying, + initialLT: LT_UNDERLYING, + finalLT: LT_UNDERLYING, + timestampRampStart: type(uint40).max, + rampDuration: 0 + }); + + creditManager.setCreditConfigurator(CONFIGURATOR); + } + + function _setUnderlying(bool underlyingIsFeeToken) internal { + uint256 oneUSDT = 10 ** _decimals(tokenTestSuite.addressOf(Tokens.USDT)); + + isFeeToken = underlyingIsFeeToken; + underlying = tokenTestSuite.addressOf(underlyingIsFeeToken ? Tokens.USDT : Tokens.DAI); + + uint256 _tokenFee = underlyingIsFeeToken ? 30_00 : 0; + uint256 _maxTokenFee = underlyingIsFeeToken ? 1000000000000 * oneUSDT : 0; + + _setFee(_tokenFee, _maxTokenFee); + + caseName = string.concat(caseName, " [fee token = ", underlyingIsFeeToken ? " true ]" : " false ]"); + } + + function _setFee(uint256 _tokenFee, uint256 _maxTokenFee) internal { + tokenFee = _tokenFee; + maxTokenFee = _maxTokenFee; + if (isFeeToken) { + ERC20FeeMock(underlying).setBasisPointsRate(tokenFee); + ERC20FeeMock(underlying).setMaximumFee(maxTokenFee); + } + } + + function _amountWithFee(uint256 amount) internal view returns (uint256) { + return isFeeToken ? amount.amountUSDTWithFee(tokenFee, maxTokenFee) : amount; + } + + function _amountMinusFee(uint256 amount) internal view returns (uint256) { + return isFeeToken ? amount.amountUSDTMinusFee(tokenFee, maxTokenFee) : amount; + } + + function _decimals(address token) internal view returns (uint8) { + return IERC20Metadata(token).decimals(); + } + + function _addToken(Tokens token, uint16 lt) internal { + _addToken(tokenTestSuite.addressOf(token), lt); + } + + function _addToken(address token, uint16 lt) internal { + vm.prank(CONFIGURATOR); + creditManager.addToken({token: token}); + + vm.prank(CONFIGURATOR); + creditManager.setCollateralTokenData({ + token: address(token), + initialLT: lt, + finalLT: lt, + timestampRampStart: type(uint40).max, + rampDuration: 0 + }); + } + + function _addQuotedToken(address token, uint16 lt, uint96 quoted, uint256 outstandingInterest) internal { + _addToken({token: token, lt: lt}); + poolQuotaKeeperMock.setQuotaAndOutstandingInterest({ + token: token, + quoted: quoted, + outstandingInterest: outstandingInterest + }); + } + + function _addQuotedToken(Tokens token, uint16 lt, uint96 quoted, uint256 outstandingInterest) internal { + _addQuotedToken({ + token: tokenTestSuite.addressOf(token), + lt: lt, + quoted: quoted, + outstandingInterest: outstandingInterest + }); + } + + function _addTokensBatch(address creditAccount, uint8 numberOfTokens, uint256 balance) internal { + for (uint8 i = 0; i < numberOfTokens; ++i) { + ERC20Mock t = + new ERC20Mock(string.concat("new token ", Strings.toString(i+1)),string.concat("NT-", Strings.toString(i+1)), 18); + + _addToken({token: address(t), lt: 80_00}); + + t.mint(creditAccount, balance * ((i + 2) % 5)); + + /// sets price between $0.01 and $60K + uint256 randomPrice = (uint256(keccak256(abi.encode(numberOfTokens, i, balance))) % 600_0000) * 10 ** 6; + priceOracleMock.setPrice(address(t), randomPrice); + } + } + + function _getTokenMaskOrRevert(Tokens token) internal view returns (uint256) { + return creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(token)); + } + + function _taskName(CollateralCalcTask task) internal pure returns (string memory) { + if (task == CollateralCalcTask.GENERIC_PARAMS) return "GENERIC_PARAMS"; + + 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.FULL_COLLATERAL_CHECK_LAZY) return "FULL_COLLATERAL_CHECK_LAZY"; + + revert("UNKNOWN TASK"); + } + + /// + /// + /// TESTS + /// + /// + + /// @dev U:[CM-1]: credit manager reverts if were called non-creditFacade + function test_U_CM_01_constructor_sets_correct_values() public allQuotaCases { + assertEq(address(creditManager.poolService()), address(poolMock), _testCaseErr("Incorrect poolService")); + + assertEq(address(creditManager.pool()), address(poolMock), _testCaseErr("Incorrect pool")); + + assertEq(creditManager.underlying(), tokenTestSuite.addressOf(Tokens.DAI), _testCaseErr("Incorrect underlying")); + + (address token, uint16 lt) = creditManager.collateralTokensByMask(UNDERLYING_TOKEN_MASK); + + assertEq(token, tokenTestSuite.addressOf(Tokens.DAI), _testCaseErr("Incorrect underlying")); + + assertEq( + creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(Tokens.DAI)), + 1, + _testCaseErr("Incorrect token mask for underlying token") + ); + + // assertEq(lt, 0, _testCaseErr("Incorrect LT for underlying")); + + assertEq(creditManager.supportsQuotas(), supportsQuotas, _testCaseErr("Incorrect supportsQuotas")); + + assertEq( + creditManager.weth(), + addressProvider.getAddressOrRevert(AP_WETH_TOKEN, 0), + _testCaseErr("Incorrect WETH token") + ); + + assertEq( + address(creditManager.wethGateway()), + addressProvider.getAddressOrRevert(AP_WETH_GATEWAY, 3_00), + _testCaseErr("Incorrect WETH Gateway") + ); + + assertEq( + address(creditManager.priceOracle()), + addressProvider.getAddressOrRevert(AP_PRICE_ORACLE, 2), + _testCaseErr("Incorrect Price oracle") + ); + + assertEq( + address(creditManager.accountFactory()), address(accountFactory), _testCaseErr("Incorrect account factory") + ); + + assertEq( + address(creditManager.creditConfigurator()), + address(CONFIGURATOR), + _testCaseErr("Incorrect creditConfigurator") + ); + } + + // + // + // MODIFIERS + // + // + + /// @dev U:[CM-2]:credit account management functions revert if were called non-creditFacade + function test_U_CM_02_credit_account_management_functions_revert_if_not_called_by_creditFacadeCall() + public + withoutSupportQuotas + { + vm.startPrank(USER); + + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.openCreditAccount(200000, address(this)); + + CollateralDebtData memory collateralDebtData; + + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.closeCreditAccount({ + creditAccount: DUMB_ADDRESS, + closureAction: ClosureAction.LIQUIDATE_ACCOUNT, + collateralDebtData: collateralDebtData, + payer: DUMB_ADDRESS, + to: DUMB_ADDRESS, + skipTokensMask: 0, + convertToETH: false + }); + + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.manageDebt(DUMB_ADDRESS, 100, 0, ManageDebtAction.INCREASE_DEBT); + + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.addCollateral(DUMB_ADDRESS, DUMB_ADDRESS, DUMB_ADDRESS, 100); + + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.transferAccountOwnership(DUMB_ADDRESS, DUMB_ADDRESS); + + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.fullCollateralCheck(DUMB_ADDRESS, 0, new uint256[](0), 1); + + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.updateQuota(DUMB_ADDRESS, DUMB_ADDRESS, 0); + + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.scheduleWithdrawal(DUMB_ADDRESS, DUMB_ADDRESS, 0); + + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.claimWithdrawals(DUMB_ADDRESS, DUMB_ADDRESS, ClaimAction.CLAIM); + + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.revokeAdapterAllowances(DUMB_ADDRESS, new RevocationPair[](0)); + + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.setActiveCreditAccount(DUMB_ADDRESS); + + vm.expectRevert(CallerNotCreditFacadeException.selector); + creditManager.setFlagFor(DUMB_ADDRESS, 1, true); + vm.stopPrank(); + } + + /// @dev U:[CM-3]:credit account adapter functions revert if were called non-adapters + function test_U_CM_03_credit_account_adapter_functions_revert_if_not_called_by_adapters() + public + withoutSupportQuotas + { + vm.startPrank(USER); + + vm.expectRevert(CallerNotAdapterException.selector); + creditManager.approveCreditAccount(DUMB_ADDRESS, 100); + + vm.expectRevert(CallerNotAdapterException.selector); + creditManager.executeOrder(bytes("0")); + + vm.stopPrank(); + } + + /// @dev U:[CM-4]: credit account configuration functions revert if were called non-configurator + function test_U_CM_04_credit_account_configurator_functions_revert_if_not_called_by_creditConfigurator() + public + withoutSupportQuotas + { + vm.expectRevert(CallerNotConfiguratorException.selector); + creditManager.addToken(DUMB_ADDRESS); + + vm.expectRevert(CallerNotConfiguratorException.selector); + creditManager.setFees(0, 0, 0, 0, 0); + + vm.expectRevert(CallerNotConfiguratorException.selector); + creditManager.setCollateralTokenData(DUMB_ADDRESS, 0, 0, 0, 0); + + vm.expectRevert(CallerNotConfiguratorException.selector); + creditManager.setQuotedMask(0); + + vm.expectRevert(CallerNotConfiguratorException.selector); + creditManager.setMaxEnabledTokens(255); + + vm.expectRevert(CallerNotConfiguratorException.selector); + creditManager.setContractAllowance(DUMB_ADDRESS, DUMB_ADDRESS); + + vm.expectRevert(CallerNotConfiguratorException.selector); + creditManager.setCreditFacade(DUMB_ADDRESS); + + vm.expectRevert(CallerNotConfiguratorException.selector); + creditManager.setPriceOracle(DUMB_ADDRESS); + + vm.expectRevert(CallerNotConfiguratorException.selector); + creditManager.setCreditConfigurator(DUMB_ADDRESS); + } + + /// @dev U:[CM-5]: non-reentrant functions revert if called in reentrancy + function test_U_CM_05_non_reentrant_functions_revert_if_called_in_reentrancy() public withoutSupportQuotas { + creditManager.setReentrancy(ENTERED); + + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.openCreditAccount(200000, address(this)); + + CollateralDebtData memory collateralDebtData; + + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.closeCreditAccount({ + creditAccount: DUMB_ADDRESS, + closureAction: ClosureAction.LIQUIDATE_ACCOUNT, + collateralDebtData: collateralDebtData, + payer: DUMB_ADDRESS, + to: DUMB_ADDRESS, + skipTokensMask: 0, + convertToETH: false + }); + + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.manageDebt(DUMB_ADDRESS, 100, 0, ManageDebtAction.INCREASE_DEBT); + + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.addCollateral(DUMB_ADDRESS, DUMB_ADDRESS, DUMB_ADDRESS, 100); + + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.transferAccountOwnership(DUMB_ADDRESS, DUMB_ADDRESS); + + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.fullCollateralCheck(DUMB_ADDRESS, 0, new uint256[](0), 1); + + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.updateQuota(DUMB_ADDRESS, DUMB_ADDRESS, 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); + + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.revokeAdapterAllowances(DUMB_ADDRESS, new RevocationPair[](0)); + + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.setActiveCreditAccount(DUMB_ADDRESS); + + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.setFlagFor(DUMB_ADDRESS, 1, true); + + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.approveCreditAccount(DUMB_ADDRESS, 100); + + vm.expectRevert("ReentrancyGuard: reentrant call"); + creditManager.executeOrder(bytes("0")); + } + + // + // + // OPEN CREDIT ACCOUNT + // + // + + /// @dev U:[CM-6]: open credit account works as expected + function test_U_CM_06_open_credit_account_works_as_expected() public allQuotaCases { + uint256 cumulativeIndexNow = RAY * 5; + poolMock.setCumulativeIndexNow(cumulativeIndexNow); + + tokenTestSuite.mint(Tokens.DAI, address(poolMock), DAI_ACCOUNT_AMOUNT); + + assertEq(creditManager.creditAccounts().length, 0, _testCaseErr("SETUP: incorrect creditAccounts() length")); + + uint256 cumulativeQuotaInterestBefore = 123412321; + uint256 enabledTokensMaskBefore = 231423; + + creditManager.setCreditAccountInfoMap({ + creditAccount: accountFactory.usedAccount(), + debt: 12039120, + cumulativeIndexLastUpdate: 23e3, + cumulativeQuotaInterest: cumulativeQuotaInterestBefore, + enabledTokensMask: enabledTokensMaskBefore, + flags: 34343, + borrower: address(0) + }); + + // todo: check why expectCall doesn't work + // vm.expectCall(address(accountFactory), abi.encodeCall(IAccountFactory.takeCreditAccount, (0, 0))); + address creditAccount = creditManager.openCreditAccount(DAI_ACCOUNT_AMOUNT, USER); + assertEq( + address(creditAccount), accountFactory.usedAccount(), _testCaseErr("Incorrect credit account returned") + ); + + ( + uint256 debt, + uint256 cumulativeIndexLastUpdate, + uint256 cumulativeQuotaInterest, + uint256 enabledTokensMask, + uint16 flags, + address borrower + ) = creditManager.creditAccountInfo(creditAccount); + + assertEq(debt, DAI_ACCOUNT_AMOUNT, _testCaseErr("Incorrect debt")); + assertEq(cumulativeIndexLastUpdate, cumulativeIndexNow, _testCaseErr("Incorrect cumulativeIndexLastUpdate")); + assertEq( + cumulativeQuotaInterest, + supportsQuotas ? 1 : cumulativeQuotaInterestBefore, + _testCaseErr("Incorrect cumulativeQuotaInterest") + ); + assertEq(enabledTokensMask, enabledTokensMaskBefore, _testCaseErr("Incorrect enabledTokensMask")); + assertEq(flags, 0, _testCaseErr("Incorrect flags")); + assertEq(borrower, USER, _testCaseErr("Incorrect borrower")); + + assertEq(poolMock.lendAmount(), DAI_ACCOUNT_AMOUNT, _testCaseErr("Incorrect amount was borrowed")); + assertEq(poolMock.lendAccount(), creditAccount, _testCaseErr("Incorrect amount was borrowed")); + + assertEq(creditManager.creditAccounts().length, 1, _testCaseErr("incorrect creditAccounts() length")); + assertEq(creditManager.creditAccounts()[0], creditAccount, _testCaseErr("incorrect creditAccounts()[0] value")); + + expectBalance(Tokens.DAI, creditAccount, DAI_ACCOUNT_AMOUNT, _testCaseErr("incorrect balance on creditAccount")); + } + + // // + // // + // // 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 allQuotaCases { + CollateralDebtData memory collateralDebtData; + + vm.expectRevert(CreditAccountNotExistsException.selector); + creditManager.closeCreditAccount({ + creditAccount: USER, + closureAction: ClosureAction.CLOSE_ACCOUNT, + collateralDebtData: collateralDebtData, + payer: DUMB_ADDRESS, + to: DUMB_ADDRESS, + skipTokensMask: 0, + convertToETH: false + }); + } + + struct CloseCreditAccountTestCase { + string name; + ClosureAction closureAction; + uint256 debt; + uint256 accruedInterest; + uint256 accruedFees; + uint256 totalValue; + uint256 enabledTokensMask; + address[] quotedTokens; + uint256 underlyingBalance; + // EXPECTED + bool expectedSetLimitsToZero; + } + + /// @dev U:[CM-8]: close credit account works as expected + function test_U_CM_08_close_credit_correctly_makes_payments() public withFeeTokenCase withSupportQuotas { + uint256 debt = DAI_ACCOUNT_AMOUNT; + + vm.assume(debt > 1_000); + vm.assume(debt < 10 ** 10 * (10 ** _decimals(underlying))); + + if (isFeeToken) { + _setFee(debt % 50_00, debt / (debt % 49 + 1)); + } + + address[] memory hasQuotedTokens = new address[](2); + + 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, + debt: debt, + accruedInterest: 0, + accruedFees: 0, + totalValue: 0, + enabledTokensMask: UNDERLYING_TOKEN_MASK, + quotedTokens: hasQuotedTokens, + underlyingBalance: debt + 1, + // EXPECTED + expectedSetLimitsToZero: false + }), + CloseCreditAccountTestCase({ + name: "Closure case for account with with charging payer", + closureAction: ClosureAction.CLOSE_ACCOUNT, + debt: debt, + accruedInterest: 0, + accruedFees: 0, + totalValue: 0, + enabledTokensMask: UNDERLYING_TOKEN_MASK, + quotedTokens: hasQuotedTokens, + underlyingBalance: debt / 2, + // EXPECTED + expectedSetLimitsToZero: false + }), + CloseCreditAccountTestCase({ + name: "Liquidate account with profit", + closureAction: ClosureAction.LIQUIDATE_ACCOUNT, + debt: debt, + accruedInterest: 0, + accruedFees: 0, + totalValue: debt * 2, + enabledTokensMask: UNDERLYING_TOKEN_MASK, + quotedTokens: hasQuotedTokens, + underlyingBalance: debt * 2, + // EXPECTED + expectedSetLimitsToZero: false + }), + CloseCreditAccountTestCase({ + name: "Liquidate account with profit, liquidator pays, with quotedTokens", + closureAction: ClosureAction.LIQUIDATE_ACCOUNT, + debt: debt, + accruedInterest: 0, + accruedFees: 0, + totalValue: _amountWithFee(debt * 100 / 95), + enabledTokensMask: UNDERLYING_TOKEN_MASK, + quotedTokens: hasQuotedTokens, + underlyingBalance: 0, + // EXPECTED + expectedSetLimitsToZero: false + }), + CloseCreditAccountTestCase({ + name: "Liquidate account with loss, no quoted tokens", + closureAction: ClosureAction.LIQUIDATE_ACCOUNT, + debt: debt, + accruedInterest: 0, + accruedFees: 0, + totalValue: debt / 2, + enabledTokensMask: UNDERLYING_TOKEN_MASK, + quotedTokens: new address[](0), + underlyingBalance: debt / 2, + // EXPECTED + expectedSetLimitsToZero: false + }), + CloseCreditAccountTestCase({ + name: "Liquidate account with loss, with quotaTokens", + closureAction: ClosureAction.LIQUIDATE_ACCOUNT, + debt: debt, + accruedInterest: 0, + accruedFees: 0, + totalValue: debt / 2, + enabledTokensMask: UNDERLYING_TOKEN_MASK, + quotedTokens: hasQuotedTokens, + underlyingBalance: debt / 2, + // EXPECTED + expectedSetLimitsToZero: true + }), + CloseCreditAccountTestCase({ + name: "Liquidate account with loss, with quotaTokens, Liquidator pays", + closureAction: ClosureAction.LIQUIDATE_ACCOUNT, + debt: debt, + accruedInterest: 0, + accruedFees: 0, + totalValue: debt / 2, + enabledTokensMask: UNDERLYING_TOKEN_MASK, + quotedTokens: hasQuotedTokens, + underlyingBalance: 0, + // EXPECTED + expectedSetLimitsToZero: true + }) + ]; + + address creditAccount = accountFactory.usedAccount(); + + 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]; + + caseName = string.concat(caseName, _case.name); + + CollateralDebtData memory collateralDebtData; + collateralDebtData._poolQuotaKeeper = address(poolQuotaKeeperMock); + collateralDebtData.debt = _case.debt; + collateralDebtData.accruedInterest = _case.accruedInterest; + collateralDebtData.accruedFees = _case.accruedFees; + 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, + amountWithFeeFn: _amountWithFee, + amountMinusFeeFn: _amountMinusFee + }); + } + + tokenTestSuite.mint(underlying, creditAccount, _case.underlyingBalance); + + startTokenTrackingSession(caseName); + + expectTokenTransfer({ + reason: "debt transfer to pool", + token: underlying, + from: creditAccount, + to: address(poolMock), + amount: _amountMinusFee(amountToPool) + }); + + if (_case.underlyingBalance < amountToPool + expectedRemainingFunds + 1) { + expectTokenTransfer({ + reason: "payer to creditAccount", + token: underlying, + from: LIQUIDATOR, + to: creditAccount, + amount: amountToPool + expectedRemainingFunds - _case.underlyingBalance + 1 + }); + } else { + uint256 amount = _case.underlyingBalance - amountToPool - expectedRemainingFunds - 1; + if (amount > 1) { + expectTokenTransfer({ + reason: "transfer to caller", + token: underlying, + from: creditAccount, + to: FRIEND, + amount: amount + }); + } + } + + 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"); + + assertEq( + poolQuotaKeeperMock.call_creditAccount(), + _case.quotedTokens.length == 0 ? address(0) : creditAccount, + "Incorrect creditAccount call to PQK" + ); + + assertTrue( + poolQuotaKeeperMock.call_setLimitsToZero() == _case.expectedSetLimitsToZero, "Incorrect setLimitsToZero" + ); + + assertEq(accountFactory.returnedAccount(), creditAccount, "returnAccount wasn't called"); + + assertEq(creditManager.creditAccounts().length, 0, _testCaseErr("incorrect creditAccounts() length")); + + 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 + withoutSupportQuotas + { + bool convertToEth = (skipTokenMask % 2) != 0; + uint8 numberOfTokens = uint8(skipTokenMask % 253); + + CollateralDebtData memory collateralDebtData; + collateralDebtData.debt = DAI_ACCOUNT_AMOUNT; + + /// @notice `+2` for underlying and WETH token + collateralDebtData.enabledTokensMask = + uint256(keccak256(abi.encode(skipTokenMask))) & ((1 << (numberOfTokens + 2)) - 1); + + address creditAccount = accountFactory.usedAccount(); + + creditManager.setBorrower(creditAccount, USER); + tokenTestSuite.mint({token: underlying, to: creditAccount, amount: _amountWithFee(collateralDebtData.debt * 2)}); + + address weth = tokenTestSuite.addressOf(Tokens.WETH); + + vm.startPrank(CONFIGURATOR); + creditManager.addToken(weth); + creditManager.setCollateralTokenData(weth, 8000, 8000, type(uint40).max, 0); + + vm.stopPrank(); + + { + 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: collateralDebtData.debt - 1 + }); + } else { + expectTokenTransfer({ + reason: string.concat("transfer token ", IERC20Metadata(token).symbol()), + token: token, + from: creditAccount, + to: (convertToEth && token == weth) ? address(wethGateway) : FRIEND, + amount: balance - 1 + }); + } + } + } + + creditManager.closeCreditAccount({ + creditAccount: creditAccount, + closureAction: ClosureAction.CLOSE_ACCOUNT, + collateralDebtData: collateralDebtData, + payer: USER, + to: FRIEND, + skipTokensMask: skipTokenMask, + convertToETH: convertToEth + }); + + checkTokenTransfers({debug: true}); + } + + // + // + // MANAGE DEBT + // + // + + /// @dev U:[CM-10]: manageDebt increases debt correctly + function test_U_CM_10_manageDebt_increases_debt_correctly(uint256 amount) + public + withFeeTokenCase + withoutSupportQuotas + { + vm.assume(amount < 10 ** 10 * (10 ** _decimals(underlying))); + + address creditAccount = accountFactory.usedAccount(); + + CollateralDebtData memory collateralDebtData; + + collateralDebtData.debt = DAI_ACCOUNT_AMOUNT; + collateralDebtData.cumulativeIndexNow = RAY * 12 / 10; + collateralDebtData.cumulativeIndexLastUpdate = RAY; + + poolMock.setCumulativeIndexNow(collateralDebtData.cumulativeIndexNow); + + creditManager.setCreditAccountInfoMap({ + creditAccount: creditAccount, + debt: collateralDebtData.debt, + cumulativeIndexLastUpdate: collateralDebtData.cumulativeIndexLastUpdate, + cumulativeQuotaInterest: 0, + enabledTokensMask: 0, + flags: 0, + borrower: USER + }); + + tokenTestSuite.mint(underlying, address(poolMock), amount); + + /// @notice this test doesn't check math - it's focused on tranfers only + (uint256 expectedNewDebt, uint256 expectedCumulativeIndex) = CreditLogic.calcIncrease({ + amount: amount, + debt: collateralDebtData.debt, + cumulativeIndexNow: collateralDebtData.cumulativeIndexNow, + cumulativeIndexLastUpdate: collateralDebtData.cumulativeIndexLastUpdate + }); + + caseName = string.concat(caseName, "increase debt"); + caseName = isFeeToken ? string.concat("Fee token: ", caseName) : caseName; + + startTokenTrackingSession(caseName); + + expectTokenTransfer({ + reason: "transfer from pool to credit account ", + token: underlying, + from: address(poolMock), + to: creditAccount, + amount: _amountMinusFee(amount) + }); + + /// @notice enabledTokesMask is set to zero, because it has no impact + (uint256 newDebt, uint256 tokensToEnable, uint256 tokensToDisable) = creditManager.manageDebt({ + creditAccount: creditAccount, + amount: amount, + enabledTokensMask: 0, + action: ManageDebtAction.INCREASE_DEBT + }); + + checkTokenTransfers({debug: false}); + + assertEq(newDebt, expectedNewDebt, _testCaseErr("Incorrect new debt")); + + assertEq(poolMock.lendAmount(), amount, _testCaseErr("Incorrect lend amount")); + assertEq(poolMock.lendAccount(), creditAccount, _testCaseErr("Incorrect credit account")); + + /// @notice checking creditAccountInf update + + (uint256 debt, uint256 cumulativeIndexLastUpdate,,,,) = creditManager.creditAccountInfo(creditAccount); + + assertEq(debt, expectedNewDebt, _testCaseErr("Incorrect debt update in creditAccountInfo")); + assertEq( + cumulativeIndexLastUpdate, + expectedCumulativeIndex, + _testCaseErr("Incorrect cumulativeIndexLastUpdate update in creditAccountInfo") + ); + + assertEq(tokensToEnable, UNDERLYING_TOKEN_MASK, _testCaseErr("Incorrect tokensToEnable")); + assertEq(tokensToDisable, 0, _testCaseErr("Incorrect tokensToDisable")); + } + + /// @dev U:[CM-11]: manageDebt decreases debt correctly + function test_U_CM_11_manageDebt_decreases_debt_correctly(uint256 _amount) public withFeeTokenCase allQuotaCases { + vm.assume(_amount < 10 ** 10 * (10 ** _decimals(underlying))); + + // uint256 amount = 10000; + uint8 testCase = uint8(_amount % 3); + + /// @notice for stack optimisation + uint256 amount = _amount; + + address creditAccount = accountFactory.usedAccount(); + + CollateralDebtData memory collateralDebtData; + + collateralDebtData.debt = amount * (amount % 5 + 1); + collateralDebtData.cumulativeIndexNow = RAY * 12 / 10; + collateralDebtData.cumulativeIndexLastUpdate = RAY; + + if (supportsQuotas) { + collateralDebtData.cumulativeQuotaInterest = amount / (amount % 5 + 1); + } + + 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: initialCQI, + enabledTokensMask: 0, + flags: 0, + borrower: USER + }); + + { + uint256 amountOnAccount = amount; + if (testCase == 1) amountOnAccount++; + if (testCase == 2) amountOnAccount += amount % 500 + 2; + + tokenTestSuite.mint(underlying, creditAccount, amountOnAccount); + } + /// @notice this test doesn't check math - it's focused on tranfers only + ( + uint256 expectedNewDebt, + uint256 expectedCumulativeIndex, + uint256 expectedAmountToRepay, + uint256 expectedProfit, + uint256 expectedCumulativeQuotaInterest + ) = CreditLogic.calcDecrease({ + amount: _amountMinusFee(amount), + debt: collateralDebtData.debt, + cumulativeIndexNow: collateralDebtData.cumulativeIndexNow, + cumulativeIndexLastUpdate: collateralDebtData.cumulativeIndexLastUpdate, + cumulativeQuotaInterest: collateralDebtData.cumulativeQuotaInterest, + feeInterest: DEFAULT_FEE_INTEREST + }); + + caseName = string.concat(caseName, "decrease debt: "); + + /// @notice there are 3 cases to test + /// #0: use whole balance of undelrying asset of CA and keeps 0 + if (testCase == 0) { + caseName = string.concat(caseName, " keeps 0"); + } + /// #1: use (whole balance - 1) of undelrying asset of CA and keeps 1 + else if (testCase == 1) { + caseName = string.concat(caseName, " keeps 1"); + } + /// #2: use <(whole balance - 1) of undelrying asset of CA and keeps >1 + else if (testCase == 2) { + caseName = string.concat(caseName, " keeps >1"); + } + + startTokenTrackingSession(caseName); + + expectTokenTransfer({ + reason: "transfer from user to pool", + token: underlying, + from: creditAccount, + to: address(poolMock), + amount: _amountMinusFee(amount) + }); + + if (supportsQuotas) { + vm.expectCall( + address(poolQuotaKeeperMock), + abi.encodeCall(IPoolQuotaKeeper.accrueQuotaInterest, (creditAccount, collateralDebtData.quotedTokens)) + ); + } + + /// @notice enabledTokesMask is set to zero, because it has no impact + (uint256 newDebt, uint256 tokensToEnable, uint256 tokensToDisable) = creditManager.manageDebt({ + creditAccount: creditAccount, + amount: amount, + enabledTokensMask: 0, + action: ManageDebtAction.DECREASE_DEBT + }); + + checkTokenTransfers({debug: false}); + + assertEq(newDebt, expectedNewDebt, _testCaseErr("Incorrect new debt")); + + assertEq(poolMock.repayAmount(), expectedAmountToRepay, _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, + supportsQuotas ? (expectedCumulativeQuotaInterest + 1) : initialCQI, + _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, (testCase != 2) ? UNDERLYING_TOKEN_MASK : 0, _testCaseErr("Incorrect tokensToDisable") + ); + } + + /// @dev U:[CM-12]: manageDebt with 0 amount doesn't change anythig + function test_U_CM_12_manageDebt_with_0_amount_doesn_t_change_anythig() public withFeeTokenCase allQuotaCases { + uint256 debt = 10000; + address creditAccount = accountFactory.usedAccount(); + + CollateralDebtData memory collateralDebtData; + + collateralDebtData.debt = debt * (debt % 5 + 1); + collateralDebtData.cumulativeIndexNow = RAY * 12 / 10; + collateralDebtData.cumulativeIndexLastUpdate = RAY; + + if (supportsQuotas) { + collateralDebtData.cumulativeQuotaInterest = debt / (debt % 5 + 1); + } + + /// todo: add outstanding interest for quota token + + poolMock.setCumulativeIndexNow(collateralDebtData.cumulativeIndexNow); + + tokenTestSuite.mint(underlying, creditAccount, debt); + + /// @notice enabledTokensMask is read directly from function parameters, not from this function + creditManager.setCreditAccountInfoMap({ + creditAccount: creditAccount, + debt: collateralDebtData.debt, + cumulativeIndexLastUpdate: collateralDebtData.cumulativeIndexLastUpdate, + cumulativeQuotaInterest: collateralDebtData.cumulativeQuotaInterest + 1, + enabledTokensMask: 0, + flags: 0, + borrower: USER + }); + + for (uint256 testCase = 0; testCase < 2; testCase++) { + caseName = string.concat(caseName, "decrease debt &"); + + caseName = + testCase == 0 ? string.concat(caseName, ", INCREASE_DEBT") : string.concat(caseName, ", DECREASE_DEBT"); + + (uint256 newDebt, uint256 tokensToEnable, uint256 tokensToDisable) = creditManager.manageDebt({ + creditAccount: creditAccount, + amount: 0, + enabledTokensMask: UNDERLYING_TOKEN_MASK, + action: testCase == 0 ? ManageDebtAction.INCREASE_DEBT : ManageDebtAction.DECREASE_DEBT + }); + + assertEq( + tokensToEnable, testCase == 0 ? UNDERLYING_TOKEN_MASK : 0, _testCaseErr("Incorrect tokensToEnable") + ); + assertEq(tokensToDisable, 0, _testCaseErr("Incorrect tokensToDisable")); + + (uint256 caiDebt, uint256 caiCumulativeIndexLastUpdate, uint256 caiCumulativeQuotaInterest,,,) = + creditManager.creditAccountInfo(creditAccount); + + assertEq(newDebt, debt, _testCaseErr("Incorrect debt update in creditAccountInfo")); + assertEq(caiDebt, debt, _testCaseErr("Incorrect debt update in creditAccountInfo")); + assertEq( + caiCumulativeIndexLastUpdate, + collateralDebtData.cumulativeIndexLastUpdate, + _testCaseErr("Incorrect cumulativeIndexLastUpdate update in creditAccountInfo") + ); + + assertEq( + caiCumulativeQuotaInterest, + collateralDebtData.cumulativeQuotaInterest + 1, + _testCaseErr("Incorrect cumulativeQuotaInterest update in creditAccountInfo") + ); + } + } + + // + // ADD COLLATERAL + // + /// @dev U:[CM-13]: addCollateral works as expected + function test_U_CM_13_addCollateral_works_as_expected() public withoutSupportQuotas { + address creditAccount = DUMB_ADDRESS; + address linkToken = tokenTestSuite.addressOf(Tokens.LINK); + + uint256 amount = DAI_ACCOUNT_AMOUNT; + + tokenTestSuite.mint({token: underlying, to: USER, amount: amount}); + + vm.prank(USER); + IERC20(underlying).approve({spender: address(creditManager), amount: type(uint256).max}); + + vm.expectRevert(TokenNotAllowedException.selector); + creditManager.addCollateral({payer: USER, creditAccount: creditAccount, token: linkToken, amount: amount}); + + startTokenTrackingSession("add collateral"); + + expectTokenTransfer({ + reason: "transfer from user to pool", + token: underlying, + from: USER, + to: creditAccount, + amount: amount + }); + + uint256 tokenToEnable = + creditManager.addCollateral({payer: USER, creditAccount: creditAccount, token: underlying, amount: amount}); + + checkTokenTransfers({debug: false}); + + assertEq(tokenToEnable, UNDERLYING_TOKEN_MASK, "Incorrect tokenToEnable"); + } + + // + // APPROVE CREDIT ACCOUNT + // + + /// @dev U:[CM-15]: approveCreditAccount works as expected + function test_U_CM_15_approveCreditAccount_works_as_expected() public withoutSupportQuotas { + address creditAccount = address(new CreditAccountMock()); + address linkToken = tokenTestSuite.addressOf(Tokens.LINK); + + creditManager.setActiveCreditAccount(address(creditAccount)); + + vm.prank(CONFIGURATOR); + creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); + + /// @notice check that it reverts on unknown token + vm.prank(ADAPTER); + vm.expectRevert(TokenNotAllowedException.selector); + creditManager.approveCreditAccount({token: linkToken, amount: 20000}); + + /// @notice logic which works with different token approvals are incapsulated + /// in CreditAccountHelper librarby and tested also there + + vm.expectCall( + creditAccount, + abi.encodeCall(ICreditAccount.execute, (underlying, abi.encodeCall(IERC20.approve, (DUMB_ADDRESS, 20000)))) + ); + + vm.expectEmit(true, true, true, true); + emit ExecuteCall(underlying, abi.encodeCall(IERC20.approve, (DUMB_ADDRESS, 20000))); + + vm.prank(ADAPTER); + creditManager.approveCreditAccount({token: underlying, amount: 20000}); + } + + // + // EXECUTE + // + + /// @dev U:[CM-16]: executeOrder works as expected + function test_U_CM_16_executeOrder_works_as_expected() public withoutSupportQuotas { + address creditAccount = address(new CreditAccountMock()); + + creditManager.setActiveCreditAccount(address(creditAccount)); + + vm.prank(CONFIGURATOR); + creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); + + bytes memory dumbCallData = bytes("Hello, world"); + bytes memory expectedReturnValue = bytes("Yes,sir!"); + + CreditAccountMock(creditAccount).setReturnExecuteResult(expectedReturnValue); + + vm.expectEmit(true, false, false, false); + emit ExecuteOrder(DUMB_ADDRESS); + + vm.expectCall(creditAccount, abi.encodeCall(ICreditAccount.execute, (DUMB_ADDRESS, dumbCallData))); + + vm.expectEmit(true, true, true, true); + emit ExecuteCall(DUMB_ADDRESS, dumbCallData); + + vm.prank(ADAPTER); + bytes memory returnValue = creditManager.executeOrder(dumbCallData); + + assertEq(returnValue, expectedReturnValue, "Incorrect return value"); + } + + // + // + // FULL COLLATERAL CHECK + // + // + + /// @dev U:[CM-17]: fullCollateralCheck reverts if hf < 10K + function test_U_CM_17_fullCollateralCheck_reverts_if_hf_less_10K() public withoutSupportQuotas { + vm.expectRevert(CustomHealthFactorTooLowException.selector); + creditManager.fullCollateralCheck({ + creditAccount: DUMB_ADDRESS, + enabledTokensMask: 0, + collateralHints: new uint256[](0), + minHealthFactor: PERCENTAGE_FACTOR - 1 + }); + } + + // /// @dev U:[CM-18]: fullCollateralCheck reverts if not enough collateral otherwise saves enabledTokensMask + function test_U_CM_18_fullCollateralCheck_reverts_if_not_enough_collateral_otherwise_saves_enabledTokensMask( + uint256 amount + ) public withFeeTokenCase withoutSupportQuotas { + /// @notice This test doesn't check collateral calculation, it proves that function + /// reverts if it's not enough collateral otherwise it stores enabledTokensMask to storage + + vm.assume(amount > 0 && amount < 1e20 * WAD); + + // uint256 amount = DAI_ACCOUNT_AMOUNT; + address creditAccount = DUMB_ADDRESS; + uint8 numberOfTokens = uint8(amount % 253); + + /// @notice `+1` for underlying token + uint256 enabledTokensMask = uint256(keccak256(abi.encode(amount))) & ((1 << (numberOfTokens + 1)) - 1); + + vm.prank(CONFIGURATOR); + creditManager.setMaxEnabledTokens(numberOfTokens + 1); + + tokenTestSuite.mint({token: underlying, to: creditAccount, amount: amount}); + + /// @notice sets price 1 USD for underlying + priceOracleMock.setPrice(underlying, 10 ** 8); + + _addTokensBatch({creditAccount: creditAccount, numberOfTokens: numberOfTokens, balance: amount}); + + creditManager.setCreditAccountInfoMap({ + creditAccount: creditAccount, + debt: 100330010, + cumulativeIndexLastUpdate: RAY, + cumulativeQuotaInterest: 0, + enabledTokensMask: enabledTokensMask, + flags: 0, + borrower: USER + }); + + CollateralDebtData memory collateralDebtData = + creditManager.calcDebtAndCollateralFC(creditAccount, CollateralCalcTask.DEBT_COLLATERAL_WITHOUT_WITHDRAWALS); + + /// @notice fuzzler could find a combination which enabled tokens with zero balances, + /// which cause to twvUSD == 0 and arithmetic errr later + vm.assume(collateralDebtData.twvUSD > 0); + + creditManager.setDebt(creditAccount, collateralDebtData.twvUSD + 1); + + collateralDebtData = + creditManager.calcDebtAndCollateralFC(creditAccount, CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY); + + assertEq( + collateralDebtData.twvUSD + 1, collateralDebtData.totalDebtUSD, "SETUP: incorrect params for liquidation" + ); + + vm.expectRevert(NotEnoughCollateralException.selector); + creditManager.fullCollateralCheck({ + creditAccount: creditAccount, + enabledTokensMask: enabledTokensMask, + collateralHints: new uint256[](0), + minHealthFactor: PERCENTAGE_FACTOR + }); + + assertTrue( + creditManager.isLiquidatable(creditAccount, PERCENTAGE_FACTOR), + "isLiquidatable returns false for liqudatable acc" + ); + + /// @notice we run calcDebtAndCollateral to get enabledTokensMask as it should be after check + creditManager.setDebt(creditAccount, collateralDebtData.twvUSD - 1); + + collateralDebtData = + creditManager.calcDebtAndCollateralFC(creditAccount, CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY); + + uint256 enabledTokenMaskWithDisableTokens = collateralDebtData.enabledTokensMask; + + assertTrue( + !creditManager.isLiquidatable(creditAccount, PERCENTAGE_FACTOR), + "isLiquidatable returns true for non-liqudatable acc" + ); + + /// @notice it makes account non liquidatable and clears mask - to check that it's set + creditManager.setCreditAccountInfoMap({ + creditAccount: creditAccount, + debt: collateralDebtData.twvUSD - 1, + cumulativeIndexLastUpdate: RAY, + cumulativeQuotaInterest: 0, + enabledTokensMask: 0, + flags: 0, + borrower: USER + }); + + creditManager.fullCollateralCheck({ + creditAccount: creditAccount, + enabledTokensMask: enabledTokensMask, + collateralHints: new uint256[](0), + minHealthFactor: PERCENTAGE_FACTOR + }); + + (,,, uint256 enabledTokensMaskAfter,,) = creditManager.creditAccountInfo(creditAccount); + + assertEq(enabledTokensMaskAfter, enabledTokenMaskWithDisableTokens, "enabledTokensMask wasn't set correctly"); + } + + // + // + // CALC DEBT AND COLLATERAL + // + // + + /// @dev U:[CM-19]: calcDebtAndCollateral reverts for FULL_COLLATERAL_CHECK_LAZY + function test_U_CM_19_calcDebtAndCollateral_reverts_for_FULL_COLLATERAL_CHECK_LAZY() public withoutSupportQuotas { + vm.expectRevert(IncorrectParameterException.selector); + creditManager.calcDebtAndCollateral({ + creditAccount: DUMB_ADDRESS, + task: CollateralCalcTask.FULL_COLLATERAL_CHECK_LAZY + }); + } + + /// @dev U:[CM-20]: calcDebtAndCollateral works correctly for GENERIC_PARAMS task + function test_U_CM_20_calcDebtAndCollateral_works_correctly_for_GENERIC_PARAMS_task() public withoutSupportQuotas { + uint256 debt = DAI_ACCOUNT_AMOUNT; + uint256 cumulativeIndexNow = RAY * 12 / 10; + uint256 cumulativeIndexLastUpdate = RAY * 11 / 10; + + address creditAccount = DUMB_ADDRESS; + + creditManager.setCreditAccountInfoMap({ + creditAccount: creditAccount, + debt: debt, + cumulativeIndexLastUpdate: cumulativeIndexLastUpdate, + cumulativeQuotaInterest: 0, + enabledTokensMask: UNDERLYING_TOKEN_MASK, + flags: 0, + borrower: USER + }); + + poolMock.setCumulativeIndexNow(cumulativeIndexNow); + + CollateralDebtData memory collateralDebtData = + creditManager.calcDebtAndCollateral({creditAccount: creditAccount, task: CollateralCalcTask.GENERIC_PARAMS}); + + assertEq(collateralDebtData.debt, debt, "Incorrect debt"); + assertEq( + collateralDebtData.cumulativeIndexLastUpdate, + cumulativeIndexLastUpdate, + "Incorrect cumulativeIndexLastUpdate" + ); + assertEq(collateralDebtData.cumulativeIndexNow, cumulativeIndexNow, "Incorrect cumulativeIndexLastUpdate"); + } + + /// @dev U:[CM-21]: calcDebtAndCollateral works correctly for DEBT_ONLY task + function test_U_CM_21_calcDebtAndCollateral_works_correctly_for_DEBT_ONLY_task() public allQuotaCases { + uint256 debt = DAI_ACCOUNT_AMOUNT; + + address creditAccount = DUMB_ADDRESS; + + uint96 LINK_QUOTA = uint96(debt / 2); + uint96 STETH_QUOTA = uint96(debt / 8); + + uint256 LINK_INTEREST = debt / 8; + uint256 STETH_INTEREST = debt / 100; + uint256 INITIAL_INTEREST = 500; + + if (supportsQuotas) { + _addQuotedToken({token: Tokens.LINK, lt: 80_00, quoted: LINK_QUOTA, outstandingInterest: LINK_INTEREST}); + _addQuotedToken({token: Tokens.STETH, lt: 30_00, quoted: STETH_QUOTA, outstandingInterest: STETH_INTEREST}); + } else { + _addToken({token: Tokens.LINK, lt: 80_00}); + _addToken({token: Tokens.STETH, lt: 30_00}); + } + uint256 LINK_TOKEN_MASK = _getTokenMaskOrRevert({token: Tokens.LINK}); + uint256 STETH_TOKEN_MASK = _getTokenMaskOrRevert({token: Tokens.STETH}); + + vars.set("cumulativeIndexNow", RAY * 22 / 10); + vars.set("cumulativeIndexLastUpdate", RAY * 21 / 10); + + poolMock.setCumulativeIndexNow(vars.get("cumulativeIndexNow")); + + if (supportsQuotas) { + vm.prank(CONFIGURATOR); + creditManager.setQuotedMask(LINK_TOKEN_MASK | STETH_TOKEN_MASK); + } + + creditManager.setCreditAccountInfoMap({ + creditAccount: creditAccount, + debt: debt, + cumulativeIndexLastUpdate: vars.get("cumulativeIndexLastUpdate"), + cumulativeQuotaInterest: INITIAL_INTEREST, + enabledTokensMask: UNDERLYING_TOKEN_MASK | LINK_TOKEN_MASK | STETH_TOKEN_MASK, + flags: 0, + borrower: USER + }); + + poolMock.setCumulativeIndexNow(vars.get("cumulativeIndexNow")); + + vm.prank(CONFIGURATOR); + creditManager.setMaxEnabledTokens(3); + + CollateralDebtData memory collateralDebtData = + creditManager.calcDebtAndCollateral(creditAccount, CollateralCalcTask.DEBT_ONLY); + + assertEq( + collateralDebtData.enabledTokensMask, + UNDERLYING_TOKEN_MASK | LINK_TOKEN_MASK | STETH_TOKEN_MASK, + "Incorrect enabledTokensMask" + ); + + assertEq( + collateralDebtData._poolQuotaKeeper, + supportsQuotas ? address(poolQuotaKeeperMock) : address(0), + "Incorrect _poolQuotaKeeper" + ); + + assertEq( + collateralDebtData.quotedTokens, + supportsQuotas ? tokenTestSuite.listOf(Tokens.LINK, Tokens.STETH, Tokens.NO_TOKEN) : new address[](0), + "Incorrect quotedTokens" + ); + + assertEq( + collateralDebtData.cumulativeQuotaInterest, + supportsQuotas ? LINK_INTEREST + STETH_INTEREST + (INITIAL_INTEREST - 1) : 0, + "Incorrect cumulativeQuotaInterest" + ); + + assertEq( + collateralDebtData.quotas, + supportsQuotas ? arrayOf(LINK_QUOTA, STETH_QUOTA, 0) : new uint256[](0), + "Incorrect quotas" + ); + + assertEq( + collateralDebtData.quotedLts, + supportsQuotas ? arrayOfU16(80_00, 30_00, 0) : new uint16[](0), + "Incorrect quotedLts" + ); + + assertEq( + collateralDebtData.quotedTokensMask, + supportsQuotas ? LINK_TOKEN_MASK | STETH_TOKEN_MASK : 0, + "Incorrect quotedLts" + ); + + assertEq( + collateralDebtData.accruedInterest, + CreditLogic.calcAccruedInterest({ + amount: debt, + cumulativeIndexLastUpdate: vars.get("cumulativeIndexLastUpdate"), + cumulativeIndexNow: vars.get("cumulativeIndexNow") + }) + (supportsQuotas ? LINK_INTEREST + STETH_INTEREST + (INITIAL_INTEREST - 1) : 0), + "Incorrect accruedInterest" + ); + + assertEq( + collateralDebtData.accruedFees, + collateralDebtData.accruedInterest * DEFAULT_FEE_INTEREST / PERCENTAGE_FACTOR, + "Incorrect accruedFees" + ); + } + + struct CollateralCalcTestCase { + string name; + uint256 enabledTokensMask; + uint256 underlyingBalance; + uint256 linkBalance; + uint256 stEthBalance; + uint256 usdcBalance; + uint256 expectedTotalValueUSD; + uint256 expectedTwvUSD; + uint256 expectedEnabledTokensMask; + } + + function _collateralTestSetup(uint256 debt) internal { + vars.set("LINK_QUOTA", 10_000); + vars.set("LINK_INTEREST", debt / 8); + + vars.set("INITIAL_INTEREST", 500); + + vars.set("LINK_LT", 80_00); + if (supportsQuotas) { + _addQuotedToken({ + token: Tokens.LINK, + lt: uint16(vars.get("LINK_LT")), + quoted: uint96(vars.get("LINK_QUOTA")), + outstandingInterest: vars.get("LINK_INTEREST") + }); + } else { + _addToken({token: Tokens.LINK, lt: uint16(vars.get("LINK_LT"))}); + } + + vars.set("STETH_LT", 30_00); + _addToken({token: Tokens.STETH, lt: uint16(vars.get("STETH_LT"))}); + _addToken({token: Tokens.USDC, lt: 60_00}); + + vars.set("cumulativeIndexNow", RAY * 22 / 10); + vars.set("cumulativeIndexLastUpdate", RAY * 21 / 10); + + poolMock.setCumulativeIndexNow(vars.get("cumulativeIndexNow")); + + /// + vars.set("UNDERLYING_PRICE", 2); + priceOracleMock.setPrice({token: underlying, price: vars.get("UNDERLYING_PRICE") * (10 ** 8)}); + + vars.set("LINK_PRICE", 4); + priceOracleMock.setPrice({ + token: tokenTestSuite.addressOf(Tokens.LINK), + price: vars.get("LINK_PRICE") * (10 ** 8) + }); + + vars.set("STETH_PRICE", 3); + priceOracleMock.setPrice({ + token: tokenTestSuite.addressOf(Tokens.STETH), + price: vars.get("STETH_PRICE") * (10 ** 8) + }); + + vars.set("USDC_PRICE", 5); + priceOracleMock.setPrice({ + token: tokenTestSuite.addressOf(Tokens.USDC), + price: vars.get("USDC_PRICE") * (10 ** 8) + }); + + /// @notice Quotas are nominated in underlying token, so we use underlying price instead link one + vars.set("LINK_QUOTA_IN_USD", vars.get("LINK_QUOTA") * vars.get("UNDERLYING_PRICE")); + } + + /// @dev U:[CM-22]: calcDebtAndCollateral works correctly for DEBT_COLLATERAL* task + function test_U_CM_22_calcDebtAndCollateral_works_correctly_for_DEBT_COLLATERAL_task() public allQuotaCases { + uint256 debt = DAI_ACCOUNT_AMOUNT; + + _collateralTestSetup(debt); + + uint256 LINK_TOKEN_MASK = _getTokenMaskOrRevert({token: Tokens.LINK}); + uint256 STETH_TOKEN_MASK = _getTokenMaskOrRevert({token: Tokens.STETH}); + uint256 USDC_TOKEN_MASK = _getTokenMaskOrRevert({token: Tokens.USDC}); + + if (supportsQuotas) { + vm.prank(CONFIGURATOR); + creditManager.setQuotedMask(LINK_TOKEN_MASK); + } + + CollateralCalcTestCase[4] memory cases = [ + CollateralCalcTestCase({ + name: "Underlying token on acccount only", + enabledTokensMask: UNDERLYING_TOKEN_MASK | STETH_TOKEN_MASK | USDC_TOKEN_MASK, + underlyingBalance: debt, + linkBalance: 0, + stEthBalance: 0, + usdcBalance: 0, + expectedTotalValueUSD: vars.get("UNDERLYING_PRICE") * (debt - 1), + expectedTwvUSD: vars.get("UNDERLYING_PRICE") * (debt - 1) * LT_UNDERLYING / PERCENTAGE_FACTOR, + expectedEnabledTokensMask: UNDERLYING_TOKEN_MASK + }), + CollateralCalcTestCase({ + name: "One quoted token with balance < quota", + enabledTokensMask: LINK_TOKEN_MASK, + underlyingBalance: 0, + linkBalance: vars.get("LINK_QUOTA") / 2 / vars.get("LINK_PRICE") + 1, + stEthBalance: 0, + usdcBalance: 0, + expectedTotalValueUSD: vars.get("LINK_QUOTA") / 2, + expectedTwvUSD: vars.get("LINK_QUOTA") / 2 * vars.get("LINK_LT") / PERCENTAGE_FACTOR, + expectedEnabledTokensMask: LINK_TOKEN_MASK + }), + CollateralCalcTestCase({ + name: "One quoted token with balance > quota", + enabledTokensMask: LINK_TOKEN_MASK, + underlyingBalance: 0, + linkBalance: 2 * vars.get("LINK_QUOTA") * vars.get("UNDERLYING_PRICE") / vars.get("LINK_PRICE") + 1, + stEthBalance: 0, + usdcBalance: 0, + expectedTotalValueUSD: 2 * vars.get("LINK_QUOTA_IN_USD"), + expectedTwvUSD: (supportsQuotas ? vars.get("LINK_QUOTA_IN_USD") : (2 * vars.get("LINK_QUOTA_IN_USD"))) + * vars.get("LINK_LT") / PERCENTAGE_FACTOR, + expectedEnabledTokensMask: LINK_TOKEN_MASK + }), + CollateralCalcTestCase({ + name: "It disables non-quoted zero balance tokens", + enabledTokensMask: UNDERLYING_TOKEN_MASK | LINK_TOKEN_MASK | STETH_TOKEN_MASK | USDC_TOKEN_MASK, + underlyingBalance: 20_000, + linkBalance: 0, + stEthBalance: 20_000, + usdcBalance: 0, + expectedTotalValueUSD: (20_000 - 1) * vars.get("UNDERLYING_PRICE") + (20_000 - 1) * vars.get("STETH_PRICE"), + expectedTwvUSD: (20_000 - 1) * vars.get("UNDERLYING_PRICE") * LT_UNDERLYING / PERCENTAGE_FACTOR + + (20_000 - 1) * vars.get("STETH_PRICE") * vars.get("STETH_LT") / PERCENTAGE_FACTOR, + expectedEnabledTokensMask: UNDERLYING_TOKEN_MASK | STETH_TOKEN_MASK | (supportsQuotas ? LINK_TOKEN_MASK : 0) + }) + ]; + + address creditAccount = DUMB_ADDRESS; + + CollateralCalcTask[3] memory tasks = [ + CollateralCalcTask.DEBT_COLLATERAL_WITHOUT_WITHDRAWALS, + CollateralCalcTask.DEBT_COLLATERAL_CANCEL_WITHDRAWALS, + CollateralCalcTask.DEBT_COLLATERAL_FORCE_CANCEL_WITHDRAWALS + ]; + + for (uint256 taskIndex = 0; taskIndex < 1; ++taskIndex) { + caseName = string.concat(caseName, _taskName(tasks[taskIndex])); + for (uint256 i; i < cases.length; ++i) { + uint256 snapshot = vm.snapshot(); + + CollateralCalcTestCase memory _case = cases[i]; + caseName = string.concat(caseName, _case.name); + + creditManager.setCreditAccountInfoMap({ + creditAccount: creditAccount, + debt: debt, + cumulativeIndexLastUpdate: vars.get("cumulativeIndexLastUpdate"), + cumulativeQuotaInterest: vars.get("INITIAL_INTEREST") + 1, + enabledTokensMask: _case.enabledTokensMask, + flags: 0, + borrower: USER + }); + + tokenTestSuite.mint({token: underlying, to: creditAccount, amount: _case.underlyingBalance}); + tokenTestSuite.mint({t: Tokens.LINK, to: creditAccount, amount: _case.linkBalance}); + tokenTestSuite.mint({t: Tokens.STETH, to: creditAccount, amount: _case.stEthBalance}); + tokenTestSuite.mint({t: Tokens.USDC, to: creditAccount, amount: _case.usdcBalance}); + + CollateralDebtData memory collateralDebtData = + creditManager.calcDebtAndCollateralFC({creditAccount: creditAccount, task: tasks[taskIndex]}); + + /// @notice It checks that USD value is computed correctly + assertEq( + collateralDebtData.totalDebtUSD, + vars.get("UNDERLYING_PRICE") + * (debt + collateralDebtData.accruedInterest + collateralDebtData.accruedFees), + _testCaseErr("Incorrect totalDebtUSD") + ); + + assertEq( + collateralDebtData.totalValueUSD, + _case.expectedTotalValueUSD, + _testCaseErr("Incorrect totalValueUSD") + ); + + assertEq(collateralDebtData.twvUSD, _case.expectedTwvUSD, _testCaseErr("Incorrect twvUSD")); + + assertEq( + collateralDebtData.enabledTokensMask, + _case.expectedEnabledTokensMask, + _testCaseErr("Incorrect enabledTokensMask") + ); + + assertEq( + collateralDebtData.totalValue, + _case.expectedTotalValueUSD / vars.get("UNDERLYING_PRICE"), + _testCaseErr("Incorrect totalValueUSD") + ); + vm.revertTo(snapshot); + } + } + } + + /// @dev U:[CM-23]: calcDebtAndCollateral adds withrawal for particilar cases correctly + function test_U_CM_23_calcDebtAndCollateral_adds_withrawal_for_particilar_cases_correctly() public allQuotaCases { + uint256 debt = DAI_ACCOUNT_AMOUNT; + uint256 amount1 = 10_000; + uint256 amount2 = 999; + + _collateralTestSetup(debt); + + address creditAccount = DUMB_ADDRESS; + tokenTestSuite.mint({token: underlying, to: creditAccount, amount: 10_000}); + + for (uint256 i = 0; i < 2; ++i) { + bool setFlag = i == 1; + caseName = + string.concat(caseName, "withdrawal computation. WITHDRAWAL FLAG is ", setFlag ? "true" : "flase"); + + creditManager.setCreditAccountInfoMap({ + creditAccount: creditAccount, + debt: debt, + cumulativeIndexLastUpdate: vars.get("cumulativeIndexLastUpdate"), + cumulativeQuotaInterest: vars.get("INITIAL_INTEREST") + 1, + enabledTokensMask: UNDERLYING_TOKEN_MASK, + flags: setFlag ? WITHDRAWAL_FLAG : 0, + borrower: USER + }); + + withdrawalManager.setCancellableWithdrawals( + false, tokenTestSuite.addressOf(Tokens.LINK), amount1, tokenTestSuite.addressOf(Tokens.STETH), amount2 + ); + + withdrawalManager.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 + }); + + 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") + ); + } + } + + /// + /// GET QUOTED TOKENS DATA + /// + + struct GetQuotedTokenDataTestCase { + string name; + // + uint256 enabledTokensMask; + address[] expectedQuotaTokens; + uint256 expertedOutstandingQuotaInterest; + uint256[] expectedQuotas; + uint16[] expectedLts; + bool expectRevert; + } + + /// @dev U:[CM-24]: _getQuotedTokensData works correctly + function test_U_CM_24_getQuotedTokensData_works_correctly() public withSupportQuotas { + assertEq(creditManager.collateralTokensCount(), 1, "SETUP: incorrect tokens count"); + + //// LINK: [QUOTED] + _addQuotedToken({token: Tokens.LINK, lt: 80_00, quoted: 10_000, outstandingInterest: 40_000}); + uint256 LINK_TOKEN_MASK = _getTokenMaskOrRevert({token: Tokens.LINK}); + + //// WETH: [NOT_QUOTED] + _addToken({token: Tokens.WETH, lt: 50_00}); + uint256 WETH_TOKEN_MASK = _getTokenMaskOrRevert({token: Tokens.WETH}); + + //// USDT: [QUOTED] + _addQuotedToken({token: Tokens.USDT, lt: 40_00, quoted: 0, outstandingInterest: 90_000}); + uint256 USDT_TOKEN_MASK = _getTokenMaskOrRevert({token: Tokens.USDT}); + + //// STETH: [QUOTED] + _addQuotedToken({token: Tokens.STETH, lt: 30_00, quoted: 20_000, outstandingInterest: 10_000}); + uint256 STETH_TOKEN_MASK = _getTokenMaskOrRevert({token: Tokens.STETH}); + + //// USDC: [NOT_QUOTED] + _addToken({token: Tokens.USDC, lt: 80_00}); + uint256 USDC_TOKEN_MASK = _getTokenMaskOrRevert({token: Tokens.USDC}); + + //// CVX: [QUOTED] + _addQuotedToken({token: Tokens.CVX, lt: 20_00, quoted: 100_000, outstandingInterest: 30_000}); + uint256 CVX_TOKEN_MASK = _getTokenMaskOrRevert({token: Tokens.CVX}); + + uint256 quotedTokensMask = LINK_TOKEN_MASK | USDT_TOKEN_MASK | STETH_TOKEN_MASK | CVX_TOKEN_MASK; + + vm.startPrank(CONFIGURATOR); + creditManager.setQuotedMask(quotedTokensMask); + creditManager.setMaxEnabledTokens(3); + vm.stopPrank(); + + // + // CASES + // + GetQuotedTokenDataTestCase[5] memory cases = [ + GetQuotedTokenDataTestCase({ + name: "No quoted tokens", + enabledTokensMask: UNDERLYING_TOKEN_MASK | WETH_TOKEN_MASK | USDC_TOKEN_MASK, + expectedQuotaTokens: new address[](0), + expertedOutstandingQuotaInterest: 0, + expectedQuotas: new uint256[](0), + expectedLts: new uint16[](0), + expectRevert: false + }), + GetQuotedTokenDataTestCase({ + name: "Revert if quoted tokens > maxEnabledTokens", + enabledTokensMask: LINK_TOKEN_MASK | USDT_TOKEN_MASK | STETH_TOKEN_MASK | CVX_TOKEN_MASK, + expectedQuotaTokens: new address[](0), + expertedOutstandingQuotaInterest: 0, + expectedQuotas: new uint256[](0), + expectedLts: new uint16[](0), + expectRevert: true + }), + GetQuotedTokenDataTestCase({ + name: "1 quotes token", + enabledTokensMask: STETH_TOKEN_MASK | WETH_TOKEN_MASK | USDC_TOKEN_MASK, + expectedQuotaTokens: tokenTestSuite.listOf(Tokens.STETH, Tokens.NO_TOKEN, Tokens.NO_TOKEN), + expertedOutstandingQuotaInterest: 10_000, + expectedQuotas: arrayOf(20_000, 0, 0), + expectedLts: arrayOfU16(30_00, 0, 0), + expectRevert: false + }), + GetQuotedTokenDataTestCase({ + name: "2 quotes token", + enabledTokensMask: STETH_TOKEN_MASK | LINK_TOKEN_MASK | WETH_TOKEN_MASK | USDC_TOKEN_MASK, + expectedQuotaTokens: tokenTestSuite.listOf(Tokens.LINK, Tokens.STETH, Tokens.NO_TOKEN), + expertedOutstandingQuotaInterest: 40_000 + 10_000, + expectedQuotas: arrayOf(10_000, 20_000, 0), + expectedLts: arrayOfU16(80_00, 30_00, 0), + expectRevert: false + }), + GetQuotedTokenDataTestCase({ + name: "3 quotes token", + enabledTokensMask: STETH_TOKEN_MASK | LINK_TOKEN_MASK | CVX_TOKEN_MASK | WETH_TOKEN_MASK | USDC_TOKEN_MASK, + expectedQuotaTokens: tokenTestSuite.listOf(Tokens.LINK, Tokens.STETH, Tokens.CVX), + expertedOutstandingQuotaInterest: 40_000 + 10_000 + 30_000, + expectedQuotas: arrayOf(10_000, 20_000, 100_000), + expectedLts: arrayOfU16(80_00, 30_00, 20_00), + expectRevert: false + }) + ]; + + for (uint256 i; i < cases.length; ++i) { + uint256 snapshot = vm.snapshot(); + + GetQuotedTokenDataTestCase memory _case = cases[i]; + + caseName = string.concat(caseName, _case.name); + + /// @notice DUMB_ADDRESS is used because poolQuotaMock has predefined returns + /// depended on token only + + if (_case.expectRevert) { + vm.expectRevert(TooManyEnabledTokensException.selector); + } + + ( + address[] memory quotaTokens, + uint256 outstandingQuotaInterest, + uint256[] memory quotas, + uint16[] memory lts, + uint256 returnedQuotedTokensMask + ) = creditManager.getQuotedTokensData({ + creditAccount: DUMB_ADDRESS, + enabledTokensMask: _case.enabledTokensMask, + _poolQuotaKeeper: address(poolQuotaKeeperMock) + }); + + if (!_case.expectRevert) { + assertEq(quotaTokens, _case.expectedQuotaTokens, _testCaseErr("Incorrect quotedTokens")); + assertEq( + outstandingQuotaInterest, + _case.expertedOutstandingQuotaInterest, + _testCaseErr("Incorrect expertedOutstandingQuotaInterest") + ); + assertEq(quotas, _case.expectedQuotas, _testCaseErr("Incorrect expectedQuotas")); + assertEq(lts, _case.expectedLts, _testCaseErr("Incorrect expectedLts")); + assertEq(returnedQuotedTokensMask, quotedTokensMask, _testCaseErr("Incorrect expectedQuotedMask")); + } + + vm.revertTo(snapshot); + } + } + + /// + /// UPDATE QUOTAS + /// + + /// @dev U:[CM-25]: updateQuota works correctly + function test_U_CM_25_updateQuota_works_correctly() public withSupportQuotas { + _addToken(Tokens.LINK, 80_00); + uint256 LINK_TOKEN_MASK = _getTokenMaskOrRevert({token: Tokens.LINK}); + + uint256 INITIAL_INTEREST = 123123; + uint256 caInterestChange = 10323212323; + address creditAccount = DUMB_ADDRESS; + + creditManager.setCreditAccountInfoMap({ + creditAccount: creditAccount, + debt: 0, + cumulativeIndexLastUpdate: 0, + cumulativeQuotaInterest: INITIAL_INTEREST, + enabledTokensMask: 0, + flags: 0, + borrower: USER + }); + + for (uint256 i = 0; i < 3; ++i) { + uint256 snapshot = vm.snapshot(); + bool enable = i == 1; + bool disable = i == 2; + uint256 expectedTokensToEnable; + uint256 expectedTokensToDisable; + + if (enable) { + caseName = string.concat(caseName, "enable case"); + expectedTokensToEnable = LINK_TOKEN_MASK; + } + if (disable) { + caseName = string.concat(caseName, "disable case"); + expectedTokensToDisable = LINK_TOKEN_MASK; + } + poolQuotaKeeperMock.setUpdateQuotaReturns(caInterestChange, enable, disable); + + /// @notice mock returns predefined values which do not depend on params + (uint256 tokensToEnable, uint256 tokensToDisable) = creditManager.updateQuota({ + creditAccount: creditAccount, + token: tokenTestSuite.addressOf(Tokens.LINK), + quotaChange: 122 + }); + + (,, uint256 cumulativeQuotaInterest,,,) = creditManager.creditAccountInfo(creditAccount); + + assertEq(tokensToEnable, expectedTokensToEnable, _testCaseErr("Incorrect tokensToEnable")); + assertEq(tokensToDisable, expectedTokensToDisable, _testCaseErr("Incorrect tokensToDisable")); + assertEq( + cumulativeQuotaInterest, + INITIAL_INTEREST + caInterestChange, + _testCaseErr("Incorrect cumulativeQuotaInterest") + ); + + vm.revertTo(snapshot); + } + } + + // + // + // WITHDRAWALS + // + // + + /// @dev U:[CM-26]: scheduleWithdrawal reverts for unknown token + function test_U_CM_26_scheduleWithdrawal_reverts_for_unknown_token() public withoutSupportQuotas { + 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}); + } + + /// @dev U:[CM-27]: scheduleWithdrawal transfers token if delay == 0 + function test_U_CM_27_scheduleWithdrawal_transfers_token_if_delay_is_zero() + public + withFeeTokenCase + withoutSupportQuotas + { + address creditAccount = DUMB_ADDRESS; + + withdrawalManager.setDelay(0); + + tokenTestSuite.mint(underlying, creditAccount, DAI_ACCOUNT_AMOUNT); + + vm.expectRevert(CreditAccountNotExistsException.selector); + (uint256 tokensToDisable) = + creditManager.scheduleWithdrawal({creditAccount: creditAccount, token: underlying, amount: 20_000}); + + // creditManager.setBorrower({creditAccount: creditAccount, borrower: USER}); + + // (tokensToDisable) = + // creditManager.scheduleWithdrawal({creditAccount: creditAccount, token: underlying, amount: 20_000}); + + // assertEq(tokensToDisable, 0, _testCaseErr("Incorrect token to disable")); + } + // + // + // + // + /// + // + // + // + // + // + // + // + // + // + // + // + /// + // + // + // + // + // + // + // + // + // + // + // + /// + // + // + // + // + // + // + // + // + // + // + // + /// + // + // + // + // + // + // + // + // + // + // + // + /// + // + // + // + // + // + // + // + + /// FROM OLD TESTS + + /// @dev U:[CM-51]: setFees sets configuration properly + function test_U_CM_51_setFees_sets_configuration_properly() public withoutSupportQuotas { + uint16 s_feeInterest = 8733; + uint16 s_feeLiquidation = 1233; + uint16 s_liquidationPremium = 1220; + uint16 s_feeLiquidationExpired = 1221; + uint16 s_liquidationPremiumExpired = 7777; + + vm.prank(CONFIGURATOR); + creditManager.setFees( + s_feeInterest, s_feeLiquidation, s_liquidationPremium, s_feeLiquidationExpired, s_liquidationPremiumExpired + ); + ( + uint16 feeInterest, + uint16 feeLiquidation, + uint16 liquidationDiscount, + uint16 feeLiquidationExpired, + uint16 liquidationPremiumExpired + ) = creditManager.fees(); + + assertEq(feeInterest, s_feeInterest, "Incorrect feeInterest"); + assertEq(feeLiquidation, s_feeLiquidation, "Incorrect feeLiquidation"); + assertEq(liquidationDiscount, s_liquidationPremium, "Incorrect liquidationDiscount"); + assertEq(feeLiquidationExpired, s_feeLiquidationExpired, "Incorrect feeLiquidationExpired"); + assertEq(liquidationPremiumExpired, s_liquidationPremiumExpired, "Incorrect liquidationPremiumExpired"); + } + + // + // ADD TOKEN + // + + /// @dev I:[CM-52]: addToken reverts if token exists and if collateralTokens > 256 + function test_I_CM_52_addToken_reverts_if_token_exists_and_if_collateralTokens_more_256() + public + withoutSupportQuotas + { + vm.startPrank(CONFIGURATOR); + + vm.expectRevert(TokenAlreadyAddedException.selector); + creditManager.addToken(underlying); + + for (uint256 i = creditManager.collateralTokensCount(); i < 255; i++) { + creditManager.addToken(address(uint160(uint256(keccak256(abi.encodePacked(i)))))); + } + + vm.expectRevert(TooManyTokensException.selector); + creditManager.addToken(DUMB_ADDRESS); + + vm.stopPrank(); + } + + /// @dev I:[CM-53]: addToken adds token and set tokenMaskMap correctly + function test_I_CM_53_addToken_adds_token_and_set_tokenMaskMap_correctly() public withoutSupportQuotas { + uint256 count = creditManager.collateralTokensCount(); + + vm.prank(CONFIGURATOR); + creditManager.addToken(DUMB_ADDRESS); + + assertEq(creditManager.collateralTokensCount(), count + 1, "collateralTokensCount want incremented"); + + assertEq(creditManager.getTokenMaskOrRevert(DUMB_ADDRESS), 1 << count, "tokenMaskMap was set incorrectly"); + } + + // + // SET LIQUIDATION THRESHOLD + // + + /// @dev I:[CM-54]: setLiquidationThreshold reverts for unknown token + function test_I_CM_54_setLiquidationThreshold_reverts_for_unknown_token() public withoutSupportQuotas { + vm.prank(CONFIGURATOR); + vm.expectRevert(TokenNotAllowedException.selector); + creditManager.setCollateralTokenData(DUMB_ADDRESS, 8000, 8000, type(uint40).max, 0); + } + + // + // CHANGE CONTRACT AllowanceAction + // + + /// @dev I:[CM-56]: setContractAllowance updates adapterToContract + function test_I_CM_56_setContractAllowance_updates_adapterToContract() public withoutSupportQuotas { + assertTrue( + creditManager.adapterToContract(ADAPTER) != DUMB_ADDRESS, "adapterToContract(ADAPTER) is already the same" + ); + + vm.prank(CONFIGURATOR); + creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); + + assertEq(creditManager.adapterToContract(ADAPTER), DUMB_ADDRESS, "adapterToContract is not set correctly"); + + assertEq(creditManager.contractToAdapter(DUMB_ADDRESS), ADAPTER, "adapterToContract is not set correctly"); + + vm.prank(CONFIGURATOR); + creditManager.setContractAllowance(ADAPTER, address(0)); + + assertEq(creditManager.adapterToContract(ADAPTER), address(0), "adapterToContract is not set correctly"); + + assertEq(creditManager.contractToAdapter(address(0)), address(0), "adapterToContract is not set correctly"); + + vm.prank(CONFIGURATOR); + creditManager.setContractAllowance(ADAPTER, DUMB_ADDRESS); + + vm.prank(CONFIGURATOR); + creditManager.setContractAllowance(address(0), DUMB_ADDRESS); + + assertEq(creditManager.adapterToContract(address(0)), address(0), "adapterToContract is not set correctly"); + + assertEq(creditManager.contractToAdapter(DUMB_ADDRESS), address(0), "adapterToContract is not set correctly"); + + // vm.prank(CONFIGURATOR); + // creditManager.setContractAllowance(ADAPTER, UNIVERSAL_CONTRACT); + + // assertEq(creditManager.universalAdapter(), ADAPTER, "Universal adapter is not correctly set"); + + // vm.prank(CONFIGURATOR); + // creditManager.setContractAllowance(address(0), UNIVERSAL_CONTRACT); + + // assertEq(creditManager.universalAdapter(), address(0), "Universal adapter is not correctly set"); + } + + // + // UPGRADE CONTRACTS + // + + /// @dev I:[CM-57A]: setCreditFacade updates Credit Facade correctly + function test_I_CM_57A_setCreditFacade_updates_contract_correctly() public withoutSupportQuotas { + assertTrue(creditManager.creditFacade() != DUMB_ADDRESS, "creditFacade( is already the same"); + + vm.prank(CONFIGURATOR); + creditManager.setCreditFacade(DUMB_ADDRESS); + + assertEq(creditManager.creditFacade(), DUMB_ADDRESS, "creditFacade is not set correctly"); + } + + /// @dev I:[CM-57B]: setPriceOracle updates contract correctly + function test_I_CM_57_setPriceOracle_updates_contract_correctly() public withoutSupportQuotas { + assertTrue(address(creditManager.priceOracle()) != DUMB_ADDRESS2, "priceOracle is already the same"); + + vm.prank(CONFIGURATOR); + creditManager.setPriceOracle(DUMB_ADDRESS2); + + assertEq(address(creditManager.priceOracle()), DUMB_ADDRESS2, "priceOracle is not set correctly"); + } + + // + // SET CONFIGURATOR + // + + /// @dev I:[CM-58]: setCreditConfigurator sets creditConfigurator correctly and emits event + function test_I_CM_58_setCreditConfigurator_sets_creditConfigurator_correctly_and_emits_event() + public + withoutSupportQuotas + { + assertTrue(creditManager.creditConfigurator() != DUMB_ADDRESS, "creditConfigurator is already the same"); + + vm.prank(CONFIGURATOR); + + vm.expectEmit(true, false, false, false); + emit SetCreditConfigurator(DUMB_ADDRESS); + + creditManager.setCreditConfigurator(DUMB_ADDRESS); + + assertEq(creditManager.creditConfigurator(), DUMB_ADDRESS, "creditConfigurator is not set correctly"); + } + + /// @dev I:[CM-61]: setMaxEnabledToken correctly sets value + function test_I_CM_61_setMaxEnabledTokens_works_correctly() public withoutSupportQuotas { + vm.prank(CONFIGURATOR); + creditManager.setMaxEnabledTokens(255); + + assertEq(creditManager.maxEnabledTokens(), 255, "Incorrect max enabled tokens"); + } +} diff --git a/contracts/test/unit/credit/CreditManagerV3Harness.sol b/contracts/test/unit/credit/CreditManagerV3Harness.sol new file mode 100644 index 00000000..e19af478 --- /dev/null +++ b/contracts/test/unit/credit/CreditManagerV3Harness.sol @@ -0,0 +1,117 @@ +pragma solidity ^0.8.17; + +import {CreditManagerV3, CreditAccountInfo} from "../../../credit/CreditManagerV3.sol"; +import {IPriceOracleV2} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; +import {CollateralDebtData, CollateralCalcTask} from "../../../interfaces/ICreditManagerV3.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; + +contract CreditManagerV3Harness is CreditManagerV3 { + using EnumerableSet for EnumerableSet.AddressSet; + + constructor(address _addressProvider, address _pool) CreditManagerV3(_addressProvider, _pool) {} + + function setReentrancy(uint8 _status) external { + _reentrancyStatus = _status; + } + + function setDebt(address creditAccount, CreditAccountInfo memory _creditAccountInfo) external { + creditAccountInfo[creditAccount] = _creditAccountInfo; + } + + function approveSpender(address token, address targetContract, address creditAccount, uint256 amount) external { + _approveSpender(token, targetContract, creditAccount, amount); + } + + function getTargetContractOrRevert() external view returns (address targetContract) { + return _getTargetContractOrRevert(); + } + + function addToCAList(address creditAccount) external { + creditAccountsSet.add(creditAccount); + } + + function setBorrower(address creditAccount, address borrower) external { + creditAccountInfo[creditAccount].borrower = borrower; + } + + function setDebt(address creditAccount, uint256 debt) external { + creditAccountInfo[creditAccount].debt = debt; + } + + function setCreditAccountInfoMap( + address creditAccount, + uint256 debt, + uint256 cumulativeIndexLastUpdate, + uint256 cumulativeQuotaInterest, + uint256 enabledTokensMask, + uint16 flags, + address borrower + ) external { + creditAccountInfo[creditAccount].debt = debt; + creditAccountInfo[creditAccount].cumulativeIndexLastUpdate = cumulativeIndexLastUpdate; + creditAccountInfo[creditAccount].cumulativeQuotaInterest = cumulativeQuotaInterest; + creditAccountInfo[creditAccount].enabledTokensMask = enabledTokensMask; + creditAccountInfo[creditAccount].flags = flags; + creditAccountInfo[creditAccount].borrower = borrower; + } + + function batchTokensTransfer(address creditAccount, address to, bool convertToETH, uint256 enabledTokensMask) + external + { + _batchTokensTransfer(creditAccount, to, convertToETH, enabledTokensMask); + } + + function safeTokenTransfer(address creditAccount, address token, address to, uint256 amount, bool convertToETH) + external + { + _safeTokenTransfer(creditAccount, token, to, amount, convertToETH); + } + + function collateralTokensByMaskCalcLT(uint256 tokenMask, bool calcLT) + external + view + returns (address token, uint16 liquidationThreshold) + { + return _collateralTokensByMask(tokenMask, calcLT); + } + + /// @dev Calculates collateral and debt parameters + function calcDebtAndCollateralFC(address creditAccount, CollateralCalcTask task) + external + view + returns (CollateralDebtData memory collateralDebtData) + { + uint256[] memory collateralHints; + + collateralDebtData = _calcDebtAndCollateral({ + creditAccount: creditAccount, + enabledTokensMask: enabledTokensMaskOf(creditAccount), + collateralHints: collateralHints, + minHealthFactor: PERCENTAGE_FACTOR, + task: task + }); + } + + function hasWithdrawals(address creditAccount) external view returns (bool) { + return _hasWithdrawals(creditAccount); + } + + function saveEnabledTokensMask(address creditAccount, uint256 enabledTokensMask) external { + _saveEnabledTokensMask(creditAccount, enabledTokensMask); + } + + function getQuotedTokensData(address creditAccount, uint256 enabledTokensMask, address _poolQuotaKeeper) + external + view + returns ( + address[] memory quotaTokens, + uint256 outstandingQuotaInterest, + uint256[] memory quotas, + uint16[] memory lts, + uint256 quotedMask + ) + { + return _getQuotedTokensData(creditAccount, enabledTokensMask, _poolQuotaKeeper); + } +} diff --git a/contracts/test/unit/credit/CreditManagerV3Harness_USDT.sol b/contracts/test/unit/credit/CreditManagerV3Harness_USDT.sol new file mode 100644 index 00000000..edd65f4f --- /dev/null +++ b/contracts/test/unit/credit/CreditManagerV3Harness_USDT.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.8.17; + +import {CreditManagerV3Harness} from "./CreditManagerV3Harness.sol"; +import {USDT_Transfer} from "../../../traits/USDT_Transfer.sol"; +import {IPoolBase} from "../../../interfaces/IPoolV3.sol"; +/// @title Credit Manager + +contract CreditManagerV3Harness_USDT is CreditManagerV3Harness, USDT_Transfer { + constructor(address _addressProvider, address _pool) + CreditManagerV3Harness(_addressProvider, _pool) + USDT_Transfer(IPoolBase(_pool).underlyingToken()) + {} + + function _amountWithFee(uint256 amount) internal view override returns (uint256) { + return _amountUSDTWithFee(amount); + } + + function _amountMinusFee(uint256 amount) internal view override returns (uint256) { + return _amountUSDTMinusFee(amount); + } +} diff --git a/contracts/test/unit/libraries/CollateralLogic.t.sol b/contracts/test/unit/libraries/CollateralLogic.t.sol new file mode 100644 index 00000000..8e716aad --- /dev/null +++ b/contracts/test/unit/libraries/CollateralLogic.t.sol @@ -0,0 +1,642 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2023 +pragma solidity ^0.8.17; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IncorrectParameterException} from "../../../interfaces/IExceptions.sol"; +import {CollateralLogic} from "../../../libraries/CollateralLogic.sol"; +import {CollateralDebtData} from "../../../interfaces/ICreditManagerV3.sol"; +import {TestHelper} from "../../lib/helper.sol"; +import {GeneralMock} from "../../mocks/GeneralMock.sol"; + +import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; +import {RAY, WAD} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; + +import "../../lib/constants.sol"; +import {Tokens} from "../../config/Tokens.sol"; +import {CollateralLogicHelper, PRICE_ORACLE, B, Q} from "./CollateralLogicHelper.sol"; + +import "forge-std/console.sol"; + +/// @title CollateralLogic unit test +contract CollateralLogicUnitTest is TestHelper, CollateralLogicHelper { + /// + /// TESTS + /// + + modifier withTokenSetup() { + _tokenSetup(); + _; + } + + function _tokenSetup() internal { + setTokenParams({t: Tokens.DAI, lt: 80_00, price: 1}); + setTokenParams({t: Tokens.USDC, lt: 60_00, price: 2}); + setTokenParams({t: Tokens.WETH, lt: 90_00, price: 1_818}); + setTokenParams({t: Tokens.LINK, lt: 30_00, price: 15}); + setTokenParams({t: Tokens.USDT, lt: 50_00, price: 1}); + setTokenParams({t: Tokens.STETH, lt: 40_00, price: 1_500}); + setTokenParams({t: Tokens.CRV, lt: 55_00, price: 10}); + } + + struct CalcOneTokenCollateralTestCase { + string name; + uint256 balance; + uint256 price; + uint16 liquidationThreshold; + uint256 quotaUSD; + // + uint256 expectedValueUSD; + uint256 expectedWeightedValueUSD; + bool expectedNonZeroBalance; + bool priceOracleCalled; + } + + /// @dev U:[CLL-1]: calcOneTokenCollateral works correctly + function test_U_CLL_01_calcOneTokenCollateral_works_correctly() public { + CalcOneTokenCollateralTestCase[4] memory cases = [ + CalcOneTokenCollateralTestCase({ + name: "Do nothing if balance == 0", + balance: 0, + price: 0, + liquidationThreshold: 80_00, + quotaUSD: 0, + // + expectedValueUSD: 0, + expectedWeightedValueUSD: 0, + expectedNonZeroBalance: false, + priceOracleCalled: false + }), + CalcOneTokenCollateralTestCase({ + name: "Do nothing if balance == 1", + balance: 0, + price: 0, + liquidationThreshold: 80_00, + quotaUSD: 0, + // + expectedValueUSD: 0, + expectedWeightedValueUSD: 0, + expectedNonZeroBalance: false, + priceOracleCalled: false + }), + CalcOneTokenCollateralTestCase({ + name: "Non quoted case, valueUSD < quotaUSD", + balance: 5000, + price: 2, + liquidationThreshold: 80_00, + quotaUSD: 10_001, + // + expectedValueUSD: (5_000 - 1) * 2, + expectedWeightedValueUSD: (5_000 - 1) * 2 * 80_00 / PERCENTAGE_FACTOR, + expectedNonZeroBalance: true, + priceOracleCalled: true + }), + CalcOneTokenCollateralTestCase({ + name: "Non quoted case, valueUSD > quotaUSD", + balance: 5000, + price: 2, + liquidationThreshold: 80_00, + quotaUSD: 40_00, + // + expectedValueUSD: (5_000 - 1) * 2, + expectedWeightedValueUSD: 40_00 * 80_00 / PERCENTAGE_FACTOR, + expectedNonZeroBalance: true, + priceOracleCalled: true + }) + ]; + + address creditAccount = makeAddr("creditAccount"); + address token = makeAddr("token"); + for (uint256 i; i < cases.length; ++i) { + uint256 snapshot = vm.snapshot(); + + CalcOneTokenCollateralTestCase memory _case = cases[i]; + caseName = _case.name; + + if (!_case.priceOracleCalled) { + revertIsPriceOracleCalled[token] = true; + } + + _prices[token] = _case.price; + + vm.mockCall(token, abi.encodeCall(IERC20.balanceOf, (creditAccount)), abi.encode(_case.balance)); + (uint256 valueUSD, uint256 weightedValueUSD, bool nonZeroBalance) = CollateralLogic.calcOneTokenCollateral({ + creditAccount: creditAccount, + convertToUSDFn: _convertToUSD, + priceOracle: PRICE_ORACLE, + token: token, + liquidationThreshold: _case.liquidationThreshold, + quotaUSD: _case.quotaUSD + }); + + assertEq(valueUSD, _case.expectedValueUSD, _testCaseErr("Incorrect valueUSD")); + assertEq(weightedValueUSD, _case.expectedWeightedValueUSD, _testCaseErr("Incorrect weightedValueUSD")); + assertEq(nonZeroBalance, _case.expectedNonZeroBalance, _testCaseErr("Incorrect nonZeroBalance")); + + vm.revertTo(snapshot); + } + } + + struct CalcOneNonQuotedTokenCollateralTestCase { + string name; + uint256 balance; + uint256 price; + uint16 liquidationThreshold; + // + uint256 expectedValueUSD; + uint256 expectedWeightedValueUSD; + bool expectedNonZeroBalance; + bool priceOracleCalled; + } + + /// @dev U:[CLL-2]: ccalcOneNonQuotedCollateral works correctly + function test_U_CLL_02_calcOneNonQuotedCollateral_works_correctly() public { + CalcOneNonQuotedTokenCollateralTestCase[3] memory cases = [ + CalcOneNonQuotedTokenCollateralTestCase({ + name: "Do nothing if balance == 0", + balance: 0, + price: 0, + liquidationThreshold: 80_00, + // + expectedValueUSD: 0, + expectedWeightedValueUSD: 0, + expectedNonZeroBalance: false, + priceOracleCalled: false + }), + CalcOneNonQuotedTokenCollateralTestCase({ + name: "Do nothing if balance == 1", + balance: 0, + price: 0, + liquidationThreshold: 80_00, + // + expectedValueUSD: 0, + expectedWeightedValueUSD: 0, + expectedNonZeroBalance: false, + priceOracleCalled: false + }), + CalcOneNonQuotedTokenCollateralTestCase({ + name: "balance > 1", + balance: 5000, + price: 2, + liquidationThreshold: 80_00, + // + expectedValueUSD: (5_000 - 1) * 2, + expectedWeightedValueUSD: (5_000 - 1) * 2 * 80_00 / PERCENTAGE_FACTOR, + expectedNonZeroBalance: true, + priceOracleCalled: true + }) + ]; + + address creditAccount = makeAddr("creditAccount"); + + for (uint256 i; i < cases.length; ++i) { + uint256 snapshot = vm.snapshot(); + + CalcOneNonQuotedTokenCollateralTestCase memory _case = cases[i]; + caseName = _case.name; + + address token = addressOf[Tokens.DAI]; + + setTokenParams({t: Tokens.DAI, lt: _case.liquidationThreshold, price: _case.price}); + + setBalances(arrayOf(B({t: Tokens.DAI, balance: _case.balance}))); + + if (!_case.priceOracleCalled) { + revertIsPriceOracleCalled[token] = true; + } + + startSession(); + + (uint256 valueUSD, uint256 weightedValueUSD, bool nonZeroBalance) = CollateralLogic + .calcOneNonQuotedCollateral({ + creditAccount: creditAccount, + convertToUSDFn: _convertToUSD, + collateralTokensByMaskFn: _collateralTokensByMask, + tokenMask: tokenMask[Tokens.DAI], + priceOracle: PRICE_ORACLE + }); + + expectTokensOrder({tokens: arrayOf(Tokens.DAI), debug: false}); + + assertEq(valueUSD, _case.expectedValueUSD, _testCaseErr("Incorrect valueUSD")); + assertEq(weightedValueUSD, _case.expectedWeightedValueUSD, _testCaseErr("Incorrect weightedValueUSD")); + assertEq(nonZeroBalance, _case.expectedNonZeroBalance, _testCaseErr("Incorrect nonZeroBalance")); + + vm.revertTo(snapshot); + } + } + + struct CalcNonQuotedTokenCollateralTestCase { + string name; + B[] balances; + uint256 limit; + uint256[] collateralHints; + uint256 tokensToCheckMask; + // expected + uint256 expectedTotalValueUSD; + uint256 expectedTwvUSD; + uint256 expectedTokensToDisable; + Tokens[] expectedOrder; + } + + /// @dev U:[CLL-3]: ccalcOneNonQuotedCollateral works correctly + function test_U_CLL_03_calcOneNonQuotedCollateral_works_correctly() public withTokenSetup { + CalcNonQuotedTokenCollateralTestCase[7] memory cases = [ + CalcNonQuotedTokenCollateralTestCase({ + name: "One token calc, no limit, no hints", + balances: arrayOf(B({t: Tokens.DAI, balance: 10_000})), + limit: type(uint256).max, + collateralHints: new uint256[](0), + tokensToCheckMask: getTokenMask(arrayOf(Tokens.DAI)), + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.DAI], + expectedTwvUSD: (10_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR, + expectedTokensToDisable: 0, + expectedOrder: arrayOf(Tokens.DAI) + }), + CalcNonQuotedTokenCollateralTestCase({ + name: "Two tokens calc, no limit, no hints", + balances: arrayOf(B({t: Tokens.DAI, balance: 10_000}), B({t: Tokens.STETH, balance: 100})), + limit: type(uint256).max, + collateralHints: new uint256[](0), + tokensToCheckMask: getTokenMask(arrayOf(Tokens.DAI, Tokens.STETH)), + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.DAI] + (100 - 1) * prices[Tokens.STETH], + expectedTwvUSD: (10_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR + + (100 - 1) * prices[Tokens.STETH] * lts[Tokens.STETH] / PERCENTAGE_FACTOR, + expectedTokensToDisable: 0, + expectedOrder: arrayOf(Tokens.DAI, Tokens.STETH) + }), + CalcNonQuotedTokenCollateralTestCase({ + name: "Disable tokens with 0 or 1 balance", + balances: arrayOf(B({t: Tokens.DAI, balance: 10_000}), B({t: Tokens.STETH, balance: 1})), + limit: type(uint256).max, + collateralHints: new uint256[](0), + tokensToCheckMask: getTokenMask(arrayOf(Tokens.DAI, Tokens.STETH, Tokens.LINK)), + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.DAI], + expectedTwvUSD: (10_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR, + expectedTokensToDisable: getTokenMask(arrayOf(Tokens.STETH, Tokens.LINK)), + expectedOrder: arrayOf(Tokens.DAI, Tokens.LINK, Tokens.STETH) + }), + CalcNonQuotedTokenCollateralTestCase({ + name: "Stops on limit", + balances: arrayOf(B({t: Tokens.DAI, balance: 10_000}), B({t: Tokens.STETH, balance: 100_000})), + limit: (10_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR, + collateralHints: new uint256[](0), + tokensToCheckMask: getTokenMask(arrayOf(Tokens.DAI, Tokens.STETH, Tokens.LINK)), + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.DAI], + expectedTwvUSD: (10_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR, + expectedTokensToDisable: 0, + expectedOrder: arrayOf(Tokens.DAI) + }), + CalcNonQuotedTokenCollateralTestCase({ + name: "Call tokens by collateral hints order", + balances: arrayOf(B({t: Tokens.DAI, balance: 10_000}), B({t: Tokens.STETH, balance: 300})), + limit: type(uint256).max, + collateralHints: getHints(arrayOf(Tokens.LINK, Tokens.DAI, Tokens.STETH)), + tokensToCheckMask: getTokenMask(arrayOf(Tokens.DAI, Tokens.STETH, Tokens.LINK)), + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.DAI] + (300 - 1) * prices[Tokens.STETH], + expectedTwvUSD: (10_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR + + (300 - 1) * prices[Tokens.STETH] * lts[Tokens.STETH] / PERCENTAGE_FACTOR, + expectedTokensToDisable: getTokenMask(arrayOf(Tokens.LINK)), + expectedOrder: arrayOf(Tokens.LINK, Tokens.DAI, Tokens.STETH) + }), + CalcNonQuotedTokenCollateralTestCase({ + name: "Call tokens by normal order after collateral hints order", + balances: arrayOf(B({t: Tokens.DAI, balance: 10_000}), B({t: Tokens.STETH, balance: 300})), + limit: type(uint256).max, + collateralHints: getHints(arrayOf(Tokens.LINK, Tokens.STETH)), + tokensToCheckMask: getTokenMask(arrayOf(Tokens.DAI, Tokens.STETH, Tokens.LINK)), + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.DAI] + (300 - 1) * prices[Tokens.STETH], + expectedTwvUSD: (10_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR + + (300 - 1) * prices[Tokens.STETH] * lts[Tokens.STETH] / PERCENTAGE_FACTOR, + expectedTokensToDisable: getTokenMask(arrayOf(Tokens.LINK)), + expectedOrder: arrayOf(Tokens.LINK, Tokens.STETH, Tokens.DAI) + }), + CalcNonQuotedTokenCollateralTestCase({ + name: "Do not double count tokens if it's mask added twice to collatreral hints", + balances: arrayOf(B({t: Tokens.DAI, balance: 10_000})), + limit: type(uint256).max, + collateralHints: getHints(arrayOf(Tokens.DAI, Tokens.DAI)), + tokensToCheckMask: getTokenMask(arrayOf(Tokens.DAI)), + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.DAI], + expectedTwvUSD: (10_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR, + expectedTokensToDisable: 0, + expectedOrder: arrayOf(Tokens.DAI) + }) + ]; + + address creditAccount = makeAddr("creditAccount"); + + for (uint256 i; i < cases.length; ++i) { + uint256 snapshot = vm.snapshot(); + + CalcNonQuotedTokenCollateralTestCase memory _case = cases[i]; + caseName = _case.name; + + setBalances(_case.balances); + + startSession(); + + (uint256 totalValueUSD, uint256 twvUSD, uint256 tokensToDisable) = CollateralLogic + .calcNonQuotedTokensCollateral({ + creditAccount: creditAccount, + limit: _case.limit, + collateralHints: _case.collateralHints, + convertToUSDFn: _convertToUSD, + collateralTokensByMaskFn: _collateralTokensByMask, + tokensToCheckMask: _case.tokensToCheckMask, + priceOracle: PRICE_ORACLE + }); + + expectTokensOrder({tokens: _case.expectedOrder, debug: false}); + + assertEq(totalValueUSD, _case.expectedTotalValueUSD, _testCaseErr("Incorrect totalValueUSD")); + assertEq(twvUSD, _case.expectedTwvUSD, _testCaseErr("Incorrect weightedValueUSD")); + assertEq(tokensToDisable, _case.expectedTokensToDisable, _testCaseErr("Incorrect nonZeroBalance")); + + vm.revertTo(snapshot); + } + } + + struct CalcQuotedTokenCollateralTestCase { + string name; + B[] balances; + Q[] quotas; + uint256 limit; + // expected + uint256 expectedTotalValueUSD; + uint256 expectedTwvUSD; + Tokens[] expectedOrder; + } + + /// @dev U:[CLL-4]: calcQuotedTokensCollateral works correctly + function test_U_CLL_04_calcQuotedTokensCollateral_works_correctly() public withTokenSetup { + CalcQuotedTokenCollateralTestCase[5] memory cases = [ + CalcQuotedTokenCollateralTestCase({ + name: "One token calc, no limit, twv < quota", + balances: arrayOf(B({t: Tokens.USDT, balance: 10_000})), + quotas: arrayOf(Q({t: Tokens.USDT, quota: 10_000})), + limit: type(uint256).max, + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.USDT], + expectedTwvUSD: (10_000 - 1) * prices[Tokens.USDT] * lts[Tokens.USDT] / PERCENTAGE_FACTOR, + expectedOrder: arrayOf(Tokens.USDT) + }), + CalcQuotedTokenCollateralTestCase({ + name: "One token calc, no limit, twv > quota", + balances: arrayOf(B({t: Tokens.USDT, balance: 10_000})), + quotas: arrayOf(Q({t: Tokens.USDT, quota: 5_000})), + limit: type(uint256).max, + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.USDT], + expectedTwvUSD: 5_000 * prices[Tokens.DAI] * lts[Tokens.USDT] / PERCENTAGE_FACTOR, + expectedOrder: arrayOf(Tokens.USDT) + }), + CalcQuotedTokenCollateralTestCase({ + name: "Two token calc, no limit, one twv quota", + balances: arrayOf(B({t: Tokens.USDT, balance: 10_000}), B({t: Tokens.LINK, balance: 1_000})), + quotas: arrayOf(Q({t: Tokens.USDT, quota: 5_000}), Q({t: Tokens.LINK, quota: 20_000})), + limit: type(uint256).max, + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.USDT] + (1_000 - 1) * prices[Tokens.LINK], + expectedTwvUSD: 5_000 * prices[Tokens.DAI] * lts[Tokens.USDT] / PERCENTAGE_FACTOR + + (1_000 - 1) * prices[Tokens.LINK] * lts[Tokens.LINK] / PERCENTAGE_FACTOR, + expectedOrder: arrayOf(Tokens.USDT, Tokens.LINK) + }), + CalcQuotedTokenCollateralTestCase({ + name: "Stops if token[i] == address(0), Tokens.NO_TOKEN has address(0)", + balances: arrayOf(B({t: Tokens.USDT, balance: 10_000})), + quotas: arrayOf( + Q({t: Tokens.WETH, quota: 10_000}), + Q({t: Tokens.NO_TOKEN, quota: 10_000}), + Q({t: Tokens.USDT, quota: 10_000}) + ), + limit: type(uint256).max, + expectedTotalValueUSD: 0, + expectedTwvUSD: 0, + expectedOrder: arrayOf(Tokens.WETH) + }), + CalcQuotedTokenCollateralTestCase({ + name: "Stops when limit reached", + balances: arrayOf(B({t: Tokens.USDT, balance: 10_000})), + quotas: arrayOf( + Q({t: Tokens.USDT, quota: 5_000}), Q({t: Tokens.WETH, quota: 50}), Q({t: Tokens.LINK, quota: 50_000}) + ), + limit: 5_000 * prices[Tokens.DAI] * lts[Tokens.USDT] / PERCENTAGE_FACTOR, + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.USDT], + expectedTwvUSD: 5_000 * prices[Tokens.DAI] * lts[Tokens.USDT] / PERCENTAGE_FACTOR, + expectedOrder: arrayOf(Tokens.USDT) + }) + ]; + + address creditAccount = makeAddr("creditAccount"); + + for (uint256 i; i < cases.length; ++i) { + uint256 snapshot = vm.snapshot(); + + CalcQuotedTokenCollateralTestCase memory _case = cases[i]; + caseName = _case.name; + + setBalances(_case.balances); + + uint256 underlyingPriceRAY = RAY * prices[Tokens.DAI]; + + CollateralDebtData memory collateralDebtData; + + collateralDebtData = setQuotas(collateralDebtData, _case.quotas); + + startSession(); + + (uint256 totalValueUSD, uint256 twvUSD) = CollateralLogic.calcQuotedTokensCollateral({ + collateralDebtData: collateralDebtData, + creditAccount: creditAccount, + underlyingPriceRAY: underlyingPriceRAY, + limit: _case.limit, + convertToUSDFn: _convertToUSD, + priceOracle: PRICE_ORACLE + }); + + expectTokensOrder({tokens: _case.expectedOrder, debug: false}); + + assertEq(totalValueUSD, _case.expectedTotalValueUSD, _testCaseErr("Incorrect totalValueUSD")); + assertEq(twvUSD, _case.expectedTwvUSD, _testCaseErr("Incorrect twvUSD")); + + vm.revertTo(snapshot); + } + } + + // + // + // CALC COLLATERAL + // + // + struct CalcCollateralTestCase { + string name; + B[] balances; + Q[] quotas; + uint256 enabledTokensMask; + uint256 quotedTokensMask; + bool lazy; + uint16 minHealthFactor; + uint256[] collateralHints; + uint256 totalDebtUSD; + // expected + uint256 expectedTotalValueUSD; + uint256 expectedTwvUSD; + uint256 expectedTokensToDisable; + Tokens[] expectedOrder; + } + + /// @dev U:[CLL-5]: calcCollateral works correctly + function test_U_CLL_05_calcCollateral_works_correctly() public withTokenSetup { + CalcCollateralTestCase[7] memory cases = [ + CalcCollateralTestCase({ + name: "One non-quoted token calc, no limit, no hints", + balances: arrayOf(B({t: Tokens.DAI, balance: 10_000})), + quotas: new Q[](0), + enabledTokensMask: getTokenMask(arrayOf(Tokens.DAI)), + quotedTokensMask: 0, + lazy: false, + minHealthFactor: PERCENTAGE_FACTOR, + collateralHints: new uint256[](0), + totalDebtUSD: 0, + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.DAI], + expectedTwvUSD: (10_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR, + expectedTokensToDisable: 0, + expectedOrder: arrayOf(Tokens.DAI) + }), + CalcCollateralTestCase({ + name: "One quoted token calc, no limit, no hints, value < quota", + balances: arrayOf(B({t: Tokens.USDT, balance: 10_000})), + quotas: arrayOf(Q({t: Tokens.USDT, quota: 10_000})), + enabledTokensMask: getTokenMask(arrayOf(Tokens.USDT)), + quotedTokensMask: getTokenMask(arrayOf(Tokens.USDT)), + lazy: false, + minHealthFactor: PERCENTAGE_FACTOR, + collateralHints: new uint256[](0), + totalDebtUSD: 0, + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.USDT], + expectedTwvUSD: (10_000 - 1) * prices[Tokens.USDT] * lts[Tokens.USDT] / PERCENTAGE_FACTOR, + expectedTokensToDisable: 0, + expectedOrder: arrayOf(Tokens.USDT) + }), + CalcCollateralTestCase({ + name: "One quoted token calc, no limit, no hints, value < quota", + balances: arrayOf(B({t: Tokens.USDT, balance: 10_000})), + quotas: arrayOf(Q({t: Tokens.USDT, quota: 5_000})), + enabledTokensMask: getTokenMask(arrayOf(Tokens.USDT)), + quotedTokensMask: getTokenMask(arrayOf(Tokens.USDT)), + lazy: false, + minHealthFactor: PERCENTAGE_FACTOR, + collateralHints: new uint256[](0), + totalDebtUSD: 0, + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.USDT], + expectedTwvUSD: 5_000 * prices[Tokens.DAI] * lts[Tokens.USDT] / PERCENTAGE_FACTOR, + expectedTokensToDisable: 0, + expectedOrder: arrayOf(Tokens.USDT) + }), + CalcCollateralTestCase({ + name: "It removes non-quoted tokens with 0 and 1 balances", + balances: arrayOf(B({t: Tokens.USDT, balance: 10_000}), B({t: Tokens.LINK, balance: 1})), + quotas: arrayOf(Q({t: Tokens.USDT, quota: 5_000})), + enabledTokensMask: getTokenMask(arrayOf(Tokens.USDT, Tokens.LINK, Tokens.DAI)), + quotedTokensMask: getTokenMask(arrayOf(Tokens.USDT)), + lazy: false, + minHealthFactor: PERCENTAGE_FACTOR, + collateralHints: new uint256[](0), + totalDebtUSD: 0, + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.USDT], + expectedTwvUSD: 5_000 * prices[Tokens.DAI] * lts[Tokens.USDT] / PERCENTAGE_FACTOR, + expectedTokensToDisable: getTokenMask(arrayOf(Tokens.LINK, Tokens.DAI)), + expectedOrder: arrayOf(Tokens.USDT, Tokens.DAI, Tokens.LINK) + }), + CalcCollateralTestCase({ + name: "It stops if limit reached during quoted token collateral computation", + balances: arrayOf(B({t: Tokens.USDT, balance: 10_000}), B({t: Tokens.LINK, balance: 1})), + quotas: arrayOf(Q({t: Tokens.USDT, quota: 5_000}), Q({t: Tokens.WETH, quota: 5_000})), + enabledTokensMask: getTokenMask(arrayOf(Tokens.USDT, Tokens.WETH, Tokens.LINK, Tokens.DAI)), + quotedTokensMask: getTokenMask(arrayOf(Tokens.USDT, Tokens.WETH)), + lazy: true, + minHealthFactor: 2 * PERCENTAGE_FACTOR, + collateralHints: new uint256[](0), + totalDebtUSD: (5_000 * prices[Tokens.DAI] * lts[Tokens.USDT] / PERCENTAGE_FACTOR) / 2, + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.USDT], + expectedTwvUSD: 5_000 * prices[Tokens.DAI] * lts[Tokens.USDT] / PERCENTAGE_FACTOR, + expectedTokensToDisable: 0, + expectedOrder: arrayOf(Tokens.USDT) + }), + CalcCollateralTestCase({ + name: "It stops if limit reached during non-quoted token collateral computation, and updates limit properly after quoted calc", + balances: arrayOf(B({t: Tokens.USDT, balance: 10_000}), B({t: Tokens.DAI, balance: 8_000})), + quotas: arrayOf(Q({t: Tokens.USDT, quota: 5_000}), Q({t: Tokens.WETH, quota: 5_000})), + enabledTokensMask: getTokenMask(arrayOf(Tokens.USDT, Tokens.WETH, Tokens.LINK, Tokens.DAI)), + quotedTokensMask: getTokenMask(arrayOf(Tokens.USDT, Tokens.WETH)), + lazy: true, + minHealthFactor: PERCENTAGE_FACTOR, + collateralHints: new uint256[](0), + totalDebtUSD: 5_000 * prices[Tokens.DAI] * lts[Tokens.USDT] / PERCENTAGE_FACTOR + + (8_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR, + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.USDT] + (8_000 - 1) * prices[Tokens.DAI], + expectedTwvUSD: 5_000 * prices[Tokens.DAI] * lts[Tokens.USDT] / PERCENTAGE_FACTOR + + (8_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR, + expectedTokensToDisable: 0, + expectedOrder: arrayOf(Tokens.USDT, Tokens.WETH, Tokens.DAI) + }), + CalcCollateralTestCase({ + name: "Collateral hints work for non-quoted tokens", + balances: arrayOf(B({t: Tokens.USDT, balance: 10_000}), B({t: Tokens.DAI, balance: 8_000})), + quotas: arrayOf(Q({t: Tokens.USDT, quota: 5_000})), + enabledTokensMask: getTokenMask(arrayOf(Tokens.USDT, Tokens.WETH, Tokens.LINK, Tokens.DAI)), + quotedTokensMask: getTokenMask(arrayOf(Tokens.USDT)), + lazy: false, + minHealthFactor: PERCENTAGE_FACTOR, + collateralHints: getHints(arrayOf(Tokens.WETH, Tokens.LINK)), + totalDebtUSD: 5_000 * prices[Tokens.DAI] * lts[Tokens.USDT] / PERCENTAGE_FACTOR + + (8_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR, + expectedTotalValueUSD: (10_000 - 1) * prices[Tokens.USDT] + (8_000 - 1) * prices[Tokens.DAI], + expectedTwvUSD: 5_000 * prices[Tokens.DAI] * lts[Tokens.USDT] / PERCENTAGE_FACTOR + + (8_000 - 1) * prices[Tokens.DAI] * lts[Tokens.DAI] / PERCENTAGE_FACTOR, + expectedTokensToDisable: getTokenMask(arrayOf(Tokens.WETH, Tokens.LINK)), + expectedOrder: arrayOf(Tokens.USDT, Tokens.WETH, Tokens.LINK, Tokens.DAI) + }) + ]; + + address creditAccount = makeAddr("creditAccount"); + + for (uint256 i; i < cases.length; ++i) { + uint256 snapshot = vm.snapshot(); + + CalcCollateralTestCase memory _case = cases[i]; + caseName = _case.name; + + setBalances(_case.balances); + + CollateralDebtData memory collateralDebtData; + + collateralDebtData.totalDebtUSD = _case.totalDebtUSD; + collateralDebtData.enabledTokensMask = _case.enabledTokensMask; + collateralDebtData.quotedTokensMask = _case.quotedTokensMask; + + collateralDebtData = setQuotas(collateralDebtData, _case.quotas); + + startSession(); + + (uint256 totalValueUSD, uint256 twvUSD, uint256 tokensToDisable) = CollateralLogic.calcCollateral({ + collateralDebtData: collateralDebtData, + creditAccount: creditAccount, + underlying: addressOf[Tokens.DAI], + lazy: _case.lazy, + minHealthFactor: _case.minHealthFactor, + collateralHints: _case.collateralHints, + convertToUSDFn: _convertToUSD, + collateralTokensByMaskFn: _collateralTokensByMask, + priceOracle: PRICE_ORACLE + }); + + expectTokensOrder({tokens: _case.expectedOrder, debug: false}); + + assertEq(totalValueUSD, _case.expectedTotalValueUSD, _testCaseErr("Incorrect totalValueUSD")); + assertEq(twvUSD, _case.expectedTwvUSD, _testCaseErr("Incorrect weightedValueUSD")); + assertEq(tokensToDisable, _case.expectedTokensToDisable, _testCaseErr("Incorrect nonZeroBalance")); + + vm.revertTo(snapshot); + } + } +} diff --git a/contracts/test/unit/libraries/CollateralLogicHelper.sol b/contracts/test/unit/libraries/CollateralLogicHelper.sol new file mode 100644 index 00000000..80f668f1 --- /dev/null +++ b/contracts/test/unit/libraries/CollateralLogicHelper.sol @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2023 +pragma solidity ^0.8.17; + +import {Tokens} from "../../config/Tokens.sol"; +import {TokensData, TestToken} from "../../config/TokensData.sol"; +import {TestHelper} from "../../lib/helper.sol"; +import {CollateralDebtData} from "../../../interfaces/ICreditManagerV3.sol"; + +import "../../lib/constants.sol"; +import {Test} from "forge-std/Test.sol"; +import "forge-std/console.sol"; + +address constant PRICE_ORACLE = DUMB_ADDRESS4; + +struct B { + Tokens t; + uint256 balance; +} + +struct Q { + Tokens t; + uint256 quota; +} + +contract CollateralLogicHelper is Test, TokensData { + uint256 session; + + mapping(Tokens => uint256) tokenMask; + + mapping(uint256 => Tokens) tokenByMask; + + mapping(Tokens => address) addressOf; + mapping(Tokens => string) symbolOf; + mapping(address => Tokens) tokenOf; + + mapping(Tokens => uint16) lts; + + mapping(address => uint256) _prices; + mapping(Tokens => uint256) prices; + + mapping(address => bool) revertIsPriceOracleCalled; + + constructor() { + TestToken[] memory tokens = tokensData(); + uint256 len = tokens.length; + for (uint256 i; i < len; ++i) { + deploy(tokens[i].index, tokens[i].symbol, uint8(i)); + } + } + + // Function helpers which're used as pointers to the lib + + function _convertToUSD(address priceOracle, uint256 amount, address token) internal view returns (uint256 result) { + require(priceOracle == PRICE_ORACLE, "Incorrect priceOracle"); + + if (revertIsPriceOracleCalled[token]) { + console.log("Price should not be fetched", token); + revert("Unexpected call to priceOracle"); + } + + result = amount * _prices[token]; + if (result == 0) { + console.log("Price for %s token is not set", token); + revert("Cant find price"); + } + } + + function _collateralTokensByMask(uint256 _tokenMask, bool calcLT) + internal + view + returns (address token, uint16 lt) + { + Tokens t = tokenByMask[_tokenMask]; + if (t == Tokens.NO_TOKEN) { + console.log("Cant find token with mask"); + console.log(_tokenMask); + revert("Token not found"); + } + + token = addressOf[t]; + if (calcLT) lt = lts[t]; + } + + /// HELPERS + + /// @dev Deployes order token and store info + function deploy(Tokens t, string memory symbol, uint8 index) internal { + address token = address(new OrderToken()); + addressOf[t] = token; + tokenOf[token] = t; + + uint256 mask = 1 << index; + + tokenMask[t] = mask; + tokenByMask[mask] = t; + + symbolOf[t] = symbol; + vm.label(addressOf[t], symbol); + } + + function setTokenParams(Tokens t, uint16 lt, uint256 price) internal { + lts[t] = lt; + prices[t] = price; + _prices[addressOf[t]] = price; + } + + function startSession() internal { + vm.record(); + } + + function saveCallOrder() external view returns (uint256) { + Tokens currentToken = tokenOf[msg.sender]; + if (currentToken == Tokens.NO_TOKEN) { + revert("Incorrect tokens order"); + } + + uint256 slot = uint256(currentToken) + 100; + + assembly { + let temp := sload(slot) + let ptr := mload(0x40) + mstore(ptr, temp) + return(ptr, 0x20) + } + } + + function expectTokensOrder(Tokens[] memory tokens, bool debug) internal { + (bytes32[] memory reads,) = vm.accesses(address(this)); + + uint256 len = reads.length; + + Tokens[] memory callOrder = new Tokens[](len); + uint256 j; + + for (uint256 i; i < len; ++i) { + uint256 slot = uint256(reads[i]); + if (slot > 100 && slot < 120) { + callOrder[j] = Tokens(slot - 100); + ++j; + } + } + + len = tokens.length; + + if (j != len) { + console.log("Different length of expected and called tokens", j, len); + console.log("Expected: "); + printTokens(tokens); + console.log("\nCall order: "); + printTokens(callOrder); + revert("Incorrect order call"); + } + + for (uint256 i; i < len; ++i) { + if (callOrder[i] != tokens[i]) { + console.log("Incorrect order of tokens calls"); + console.log("Expected: "); + printTokens(tokens); + console.log("\nCall order: "); + printTokens(callOrder); + revert("Incorrect order call"); + } + } + + if (debug) { + console.log("Tokens were called in correct order"); + printTokens(tokens); + } + } + + function printTokens(Tokens[] memory tokens) internal view { + uint256 len = tokens.length; + + for (uint256 i; i < len; ++i) { + Tokens t = Tokens(tokens[i]); + if (t == Tokens.NO_TOKEN) break; + console.log(symbolOf[t]); + } + } + + function getTokenMask(Tokens[] memory tokens) internal view returns (uint256 mask) { + uint256 len = tokens.length; + for (uint256 i; i < len; ++i) { + mask |= tokenMask[tokens[i]]; + } + } + + function getHints(Tokens[] memory tokens) internal view returns (uint256[] memory collateralHints) { + uint256 len = tokens.length; + collateralHints = new uint256[](len); + for (uint256 i; i < len; ++i) { + collateralHints[i] = tokenMask[tokens[i]]; + } + } + + function setBalances(B[] memory balances) internal { + uint256 len = balances.length; + for (uint256 i; i < len; ++i) { + OrderToken(addressOf[balances[i].t]).setBalance(balances[i].balance); + } + } + + function setQuotas(CollateralDebtData memory collateralDebtData, Q[] memory quotas) + internal + view + returns (CollateralDebtData memory) + { + uint256 len = quotas.length; + + collateralDebtData.quotedTokens = new address[](len); + collateralDebtData.quotas = new uint256[](len); + collateralDebtData.quotedLts = new uint16[](len); + + for (uint256 i; i < len; ++i) { + collateralDebtData.quotedTokens[i] = addressOf[quotas[i].t]; + collateralDebtData.quotas[i] = quotas[i].quota; + collateralDebtData.quotedLts[i] = lts[quotas[i].t]; + } + + return collateralDebtData; + } + + /// + + function arrayOf(Tokens t1) internal pure returns (Tokens[] memory result) { + result = new Tokens[](1); + result[0] = t1; + } + + function arrayOf(Tokens t1, Tokens t2) internal pure returns (Tokens[] memory result) { + result = new Tokens[](2); + result[0] = t1; + result[1] = t2; + } + + function arrayOf(Tokens t1, Tokens t2, Tokens t3) internal pure returns (Tokens[] memory result) { + result = new Tokens[](3); + result[0] = t1; + result[1] = t2; + result[2] = t3; + } + + function arrayOf(Tokens t1, Tokens t2, Tokens t3, Tokens t4) internal pure returns (Tokens[] memory result) { + result = new Tokens[](4); + result[0] = t1; + result[1] = t2; + result[2] = t3; + result[3] = t4; + } + + function arrayOf(Tokens t1, Tokens t2, Tokens t3, Tokens t4, Tokens t5) + internal + pure + returns (Tokens[] memory result) + { + result = new Tokens[](5); + result[0] = t1; + result[1] = t2; + result[2] = t3; + result[3] = t4; + result[4] = t5; + } + + function arrayOf(B memory t1) internal pure returns (B[] memory result) { + result = new B[](1); + result[0] = t1; + } + + function arrayOf(B memory t1, B memory t2) internal pure returns (B[] memory result) { + result = new B[](2); + result[0] = t1; + result[1] = t2; + } + + function arrayOf(B memory t1, B memory t2, B memory t3) internal pure returns (B[] memory result) { + result = new B[](3); + result[0] = t1; + result[1] = t2; + result[2] = t3; + } + + function arrayOf(B memory t1, B memory t2, B memory t3, B memory t4) internal pure returns (B[] memory result) { + result = new B[](4); + result[0] = t1; + result[1] = t2; + result[2] = t3; + result[3] = t4; + } + + function arrayOf(B memory t1, B memory t2, B memory t3, B memory t4, B memory t5) + internal + pure + returns (B[] memory result) + { + result = new B[](5); + result[0] = t1; + result[1] = t2; + result[2] = t3; + result[3] = t4; + result[4] = t5; + } + + function arrayOf(Q memory t1) internal pure returns (Q[] memory result) { + result = new Q[](1); + result[0] = t1; + } + + function arrayOf(Q memory t1, Q memory t2) internal pure returns (Q[] memory result) { + result = new Q[](2); + result[0] = t1; + result[1] = t2; + } + + function arrayOf(Q memory t1, Q memory t2, Q memory t3) internal pure returns (Q[] memory result) { + result = new Q[](3); + result[0] = t1; + result[1] = t2; + result[2] = t3; + } + + function arrayOf(Q memory t1, Q memory t2, Q memory t3, Q memory t4) internal pure returns (Q[] memory result) { + result = new Q[](4); + result[0] = t1; + result[1] = t2; + result[2] = t3; + result[3] = t4; + } + + function arrayOf(Q memory t1, Q memory t2, Q memory t3, Q memory t4, Q memory t5) + internal + pure + returns (Q[] memory result) + { + result = new Q[](5); + result[0] = t1; + result[1] = t2; + result[2] = t3; + result[3] = t4; + result[4] = t5; + } +} + +contract OrderToken { + uint256 returnBalance; + + address immutable orderChecker; + + constructor() { + orderChecker = msg.sender; + } + + function balanceOf(address holder) external view returns (uint256 amount) { + CollateralLogicHelper(orderChecker).saveCallOrder(); + amount = returnBalance; + } + + function setBalance(uint256 balance) external { + returnBalance = balance; + } +} diff --git a/contracts/test/unit/libraries/CreditAccountHelper.t copy.sol b/contracts/test/unit/libraries/CreditAccountHelper.t copy.sol deleted file mode 100644 index 29259147..00000000 --- a/contracts/test/unit/libraries/CreditAccountHelper.t copy.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// Gearbox Protocol. Generalized leverage for DeFi protocols -// (c) Gearbox Holdings, 2023 -pragma solidity ^0.8.17; - -import {IncorrectParameterException} from "../../../interfaces/IExceptions.sol"; -import {CreditAccountHelper} from "../../../libraries/CreditAccountHelper.sol"; -import {ICreditAccount} from "../../../interfaces/ICreditAccount.sol"; - -import {CreditAccount} from "../../../core/CreditAccount.sol"; - -import {TestHelper} from "../../lib/helper.sol"; -import "forge-std/console.sol"; - -/// @title CreditAccountHelper logic test -/// @notice [CAH]: Unit tests for credit account helper -contract CreditAccountHelperTest is TestHelper { - /// @notice U:[CL-1]: `calcIndex` reverts for zero value - function test_CL_01_calcIndex_reverts_for_zero_value() public {} -} diff --git a/contracts/test/unit/libraries/CreditAccountHelper.t.sol b/contracts/test/unit/libraries/CreditAccountHelper.t.sol new file mode 100644 index 00000000..a23ba8f0 --- /dev/null +++ b/contracts/test/unit/libraries/CreditAccountHelper.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2023 +pragma solidity ^0.8.17; + +import {IncorrectParameterException} from "../../../interfaces/IExceptions.sol"; +import {CreditAccountHelper} from "../../../libraries/CreditAccountHelper.sol"; +import {ICreditAccount} from "../../../interfaces/ICreditAccount.sol"; +import {CreditAccountV3} from "../../../credit/CreditAccountV3.sol"; + +import { + ERC20ApproveRestrictedRevert, + ERC20ApproveRestrictedFalse +} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/ERC20ApproveRestricted.sol"; + +import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; +import {Tokens} from "../../config/Tokens.sol"; +import {TestHelper} from "../../lib/helper.sol"; +import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; + +import "../../lib/constants.sol"; +import "forge-std/console.sol"; + +/// @title CreditAccountHelper logic test +/// @notice [CAH]: Unit tests for credit account helper +contract CreditAccountHelperTest is TestHelper, BalanceHelper { + using CreditAccountHelper for ICreditAccount; + + address creditAccount; + + function setUp() public { + tokenTestSuite = new TokensTestSuite(); + creditAccount = address(new CreditAccountV3(address(this))); + } + + /// @notice U:[CAH-01]: approveCreditAccount approves with desired allowance + function test_CAH_01_safeApprove_approves_with_desired_allowance() public { + // Case, when current allowance > Allowance_THRESHOLD + tokenTestSuite.approve(Tokens.DAI, creditAccount, DUMB_ADDRESS, 200); + + address dai = tokenTestSuite.addressOf(Tokens.DAI); + + ICreditAccount(creditAccount).safeApprove(dai, DUMB_ADDRESS, DAI_EXCHANGE_AMOUNT); + + expectAllowance(Tokens.DAI, creditAccount, DUMB_ADDRESS, DAI_EXCHANGE_AMOUNT); + } + + /// @dev U:[CAH-02]: approveCreditAccount works for ERC20 that revert if allowance > 0 before approve + function test_CAH_02_safeApprove_works_for_ERC20_with_approve_restrictions() public { + address approveRevertToken = address(new ERC20ApproveRestrictedRevert()); + + ICreditAccount(creditAccount).safeApprove(approveRevertToken, DUMB_ADDRESS, DAI_EXCHANGE_AMOUNT); + + ICreditAccount(creditAccount).safeApprove(approveRevertToken, DUMB_ADDRESS, 2 * DAI_EXCHANGE_AMOUNT); + + expectAllowance(approveRevertToken, creditAccount, DUMB_ADDRESS, 2 * DAI_EXCHANGE_AMOUNT); + + address approveFalseToken = address(new ERC20ApproveRestrictedFalse()); + + ICreditAccount(creditAccount).safeApprove(approveFalseToken, DUMB_ADDRESS, DAI_EXCHANGE_AMOUNT); + + ICreditAccount(creditAccount).safeApprove(approveFalseToken, DUMB_ADDRESS, 2 * DAI_EXCHANGE_AMOUNT); + + expectAllowance(approveFalseToken, creditAccount, DUMB_ADDRESS, 2 * DAI_EXCHANGE_AMOUNT); + } +} diff --git a/contracts/test/unit/libraries/CreditLogic.t.sol b/contracts/test/unit/libraries/CreditLogic.t.sol index b5742542..1acbb443 100644 --- a/contracts/test/unit/libraries/CreditLogic.t.sol +++ b/contracts/test/unit/libraries/CreditLogic.t.sol @@ -3,15 +3,725 @@ // (c) Gearbox Holdings, 2023 pragma solidity ^0.8.17; +import {IPoolQuotaKeeper} from "../../../interfaces/IPoolQuotaKeeper.sol"; +import {IPriceOracleV2} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceOracle.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + import {IncorrectParameterException} from "../../../interfaces/IExceptions.sol"; import {CreditLogic} from "../../../libraries/CreditLogic.sol"; - +import {ClosureAction, CollateralDebtData, CollateralTokenData} from "../../../interfaces/ICreditManagerV3.sol"; import {TestHelper} from "../../lib/helper.sol"; +import {GeneralMock} from "../../mocks/GeneralMock.sol"; + +import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; +import {RAY, WAD} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; + import "forge-std/console.sol"; /// @title BitMask logic test /// @notice [BM]: Unit tests for bit mask library contract CreditLogicTest is TestHelper { + uint256 public constant TEST_FEE = 50; + + CollateralTokenData ctd; + + address[8] tokens; + uint16[8] tokenLTsStorage; + uint256[8] tokenBalancesStorage; + uint256[8] tokenPricesStorage; + + function _prepareTokens() internal { + for (uint256 i; i < 8; ++i) { + tokens[i] = address(new GeneralMock()); + } + } + + function _amountWithoutFee(uint256 a) internal pure returns (uint256) { + return a; + } + + function _amountPlusFee(uint256 a) internal pure returns (uint256) { + return a * (TEST_FEE + PERCENTAGE_FACTOR) / PERCENTAGE_FACTOR; + } + + function _amountMinusFee(uint256 a) internal pure returns (uint256) { + return a * (PERCENTAGE_FACTOR - TEST_FEE) / PERCENTAGE_FACTOR; + } + + function _calcDiff(uint256 a, uint256 b) internal pure returns (uint256 diff) { + diff = a > b ? a - b : b - a; + } + + function _getTokenArray() internal view returns (address[] memory tokensMemory) { + tokensMemory = new address[](8); + + for (uint256 i = 0; i < 8; ++i) { + tokensMemory[i] = tokens[i]; + } + } + + function _getLTArray() internal view returns (uint16[] memory tokenLTsMemory) { + tokenLTsMemory = new uint16[](8); + + for (uint256 i = 0; i < 8; ++i) { + tokenLTsMemory[i] = tokenLTsStorage[i]; + } + } + + function _getBalanceArray() internal view returns (uint256[] memory tokenBalancesMemory) { + tokenBalancesMemory = new uint256[](8); + + for (uint256 i = 0; i < 8; ++i) { + tokenBalancesMemory[i] = tokenBalancesStorage[i]; + } + } + + function _getPriceArray() internal view returns (uint256[] memory tokenPricesMemory) { + tokenPricesMemory = new uint256[](8); + + for (uint256 i = 0; i < 8; ++i) { + tokenPricesMemory[i] = tokenPricesStorage[i]; + } + } + + function _getCollateralHintsIdx(uint256 rand) internal returns (uint256[] memory collateralHints) { + uint256 len = uint256(keccak256(abi.encode(rand))) % 9; + + uint256[] memory nums = new uint256[](8); + uint256[] memory collateralHints = new uint256[](len); + + for (uint256 i = 0; i < 8; ++i) { + nums[i] = i; + } + + for (uint256 i = 0; i < len; ++i) { + rand = uint256(keccak256(abi.encode(rand))); + uint256 idx = rand % (8 - i); + collateralHints[i] = 2 ** nums[idx]; + nums[idx] = nums[7 - i]; + } + } + + function _getMasksFromIdx(uint256[] memory idxArray) internal view returns (uint256[] memory masksArray) { + masksArray = new uint256[](idxArray.length); + + for (uint256 i = 0; i < idxArray.length; ++i) { + masksArray[i] = 2 ** idxArray[i]; + } + } + + function _calcTotalDebt( + uint256 debt, + uint256 indexNow, + uint256 indexOpen, + uint256 quotaInterest, + uint16 feeInterest + ) internal view returns (uint256) { + return debt + + (debt * indexNow / indexOpen + quotaInterest - debt) * (PERCENTAGE_FACTOR + feeInterest) / PERCENTAGE_FACTOR; + } + /// @notice U:[CL-1]: `calcIndex` reverts for zero value - function test_CL_01_calcIndex_reverts_for_zero_value() public {} + function test_U_CL_01_calcAccruedInterest_computes_interest_with_small_error( + uint256 debt, + uint256 cumulativeIndexAtOpen, + uint256 borrowRate, + uint256 timeDiff + ) public { + debt = 100 + debt % (2 ** 128 - 101); + cumulativeIndexAtOpen = RAY + cumulativeIndexAtOpen % (99 * RAY); + borrowRate = borrowRate % (10 * RAY); + timeDiff = timeDiff % (2000 days); + + uint256 timestampLastUpdate = block.timestamp; + + vm.warp(block.timestamp + timeDiff); + + uint256 interest = CreditLogic.calcLinearGrowth(debt * borrowRate, timestampLastUpdate) / RAY; + + uint256 cumulativeIndexNow = + cumulativeIndexAtOpen * (RAY + CreditLogic.calcLinearGrowth(borrowRate, timestampLastUpdate)) / RAY; + + uint256 diff = + _calcDiff(CreditLogic.calcAccruedInterest(debt, cumulativeIndexAtOpen, cumulativeIndexNow), interest); + + assertLe(RAY * diff / debt, 10000, "Interest error is more than 10 ** -22"); + } + + /// @notice U:[CL-2]: `calcIncrease` outputs new interest that is old interest with at most a small error + function test_U_CL_02_calcIncrease_preserves_interest( + uint256 debt, + uint256 indexNow, + uint256 indexAtOpen, + uint256 delta + ) public { + vm.assume(debt > 100); + vm.assume(debt < 2 ** 128 - 1); + vm.assume(delta < 2 ** 128 - 1); + vm.assume(debt + delta <= 2 ** 128 - 1); + + indexNow = indexNow < RAY ? indexNow + RAY : indexNow; + indexAtOpen = indexAtOpen < RAY ? indexAtOpen + RAY : indexAtOpen; + + vm.assume(indexNow <= 100 * RAY); + vm.assume(indexNow >= indexAtOpen); + vm.assume(indexNow - indexAtOpen < 10 * RAY); + + uint256 interest = uint256((debt * indexNow) / indexAtOpen - debt); + + vm.assume(interest > 1); + + (uint256 newDebt, uint256 newIndex) = CreditLogic.calcIncrease(delta, debt, indexNow, indexAtOpen); + + assertEq(newDebt, debt + delta, "Debt principal not updated correctly"); + + uint256 newInterestError = (newDebt * indexNow) / newIndex - newDebt - ((debt * indexNow) / indexAtOpen - debt); + + uint256 newTotalDebt = (newDebt * indexNow) / newIndex; + + assertLe((RAY * newInterestError) / newTotalDebt, 10000, "Interest error is larger than 10 ** -23"); + } + + /// @notice U:[CL-3A]: `calcDecrease` outputs newTotalDebt that is different by delta with at most a small error + function test_U_CL_03A_calcDecrease_outputs_correct_new_total_debt( + uint256 debt, + uint256 indexNow, + uint256 indexAtOpen, + uint256 delta, + uint256 quotaInterest, + uint16 feeInterest + ) public { + debt = WAD + debt % (2 ** 128 - WAD - 1); + delta = delta % (2 ** 128 - 1); + quotaInterest = quotaInterest % (2 ** 128 - 1); + + vm.assume(debt + delta <= 2 ** 128 - 1); + + feeInterest %= PERCENTAGE_FACTOR + 1; + + indexNow = indexNow < RAY ? indexNow + RAY : indexNow; + indexAtOpen = indexAtOpen < RAY ? indexAtOpen + RAY : indexAtOpen; + + indexNow %= 100 * RAY + 1; + + vm.assume(indexNow >= indexAtOpen); + vm.assume(indexNow - indexAtOpen < 10 * RAY); + + uint256 interest = uint256((debt * indexNow) / indexAtOpen - debt); + + vm.assume(interest > 1); + + if (delta > debt + interest + quotaInterest) delta %= debt + interest + quotaInterest; + + (uint256 newDebt, uint256 newCumulativeIndex,,, uint256 cumulativeQuotaInterest) = + CreditLogic.calcDecrease(delta, debt, indexNow, indexAtOpen, quotaInterest, feeInterest); + + uint256 oldTotalDebt = _calcTotalDebt(debt, indexNow, indexAtOpen, quotaInterest, feeInterest); + uint256 newTotalDebt = + _calcTotalDebt(newDebt, indexNow, newCumulativeIndex, cumulativeQuotaInterest, feeInterest); + + uint256 debtError = _calcDiff(oldTotalDebt, newTotalDebt + delta); + uint256 rel = oldTotalDebt > newTotalDebt ? oldTotalDebt : newTotalDebt; + + debtError = debtError > 10 ? debtError : 0; + + assertLe((RAY * debtError) / rel, 10 ** 5, "Error is larger than 10 ** -22"); + } + + /// @notice U:[CL-3B]: `calcDecrease` correctly outputs amountToRepay and profit + function test_U_CL_03B_calcDecrease_outputs_correct_amountToRepay_profit( + uint256 debt, + uint256 indexNow, + uint256 indexAtOpen, + uint256 delta, + uint256 quotaInterest, + uint16 feeInterest + ) public { + debt = WAD + debt % (2 ** 128 - WAD - 1); + delta = delta % (2 ** 128 - 1); + quotaInterest = quotaInterest % (2 ** 128 - 1); + + vm.assume(debt + delta <= 2 ** 128 - 1); + + feeInterest %= PERCENTAGE_FACTOR + 1; + + indexNow = indexNow < RAY ? indexNow + RAY : indexNow; + indexAtOpen = indexAtOpen < RAY ? indexAtOpen + RAY : indexAtOpen; + + indexNow %= 100 * RAY + 1; + + vm.assume(indexNow >= indexAtOpen); + vm.assume(indexNow - indexAtOpen < 10 * RAY); + + uint256 interest = uint256((debt * indexNow) / indexAtOpen - debt); + + vm.assume(interest > 1); + + if (delta > debt + interest + quotaInterest) delta %= debt + interest + quotaInterest; + + (uint256 newDebt,, uint256 amountToRepay, uint256 profit,) = + CreditLogic.calcDecrease(delta, debt, indexNow, indexAtOpen, quotaInterest, feeInterest); + + assertEq(amountToRepay, debt - newDebt, "Amount to repay incorrect"); + + uint256 expectedProfit = delta + > (interest + quotaInterest) * (PERCENTAGE_FACTOR + feeInterest) / PERCENTAGE_FACTOR + ? (interest + quotaInterest) * feeInterest / PERCENTAGE_FACTOR + : delta * feeInterest / (PERCENTAGE_FACTOR + feeInterest); + + uint256 profitError = _calcDiff(expectedProfit, profit); + + assertLe(profitError, 100, "Profit error too large"); + } + + struct CalcLiquidationPaymentsTestCase { + string name; + bool withFee; + uint16 liquidationDiscount; + uint16 feeLiquidation; + uint16 feeInterest; + uint256 totalValue; + uint256 debt; + uint256 accruedInterest; + uint256 amountToPool; + uint256 remainingFunds; + uint256 profit; + uint256 loss; + } + + /// @notice U:[CL-4]: `calcLiquidationPayments` gives expected outputs + function test_U_CL_04_calcLiquidationPayments_case_test() public { + /// FEE INTEREST: 50% + /// NORMAL LIQUIDATION PREMIUM: 4% + /// NORMAL LIQUIDATION FEE: 1.5% + /// EXPIRE LIQUIDATION PREMIUM: 2% + /// EXPIRE LIQUIDATION FEE: 1% + /// TOKEN FEE: 0.5% + + CalcLiquidationPaymentsTestCase[9] memory cases = [ + CalcLiquidationPaymentsTestCase({ + name: "NORMAL LIQUIDATION WITH PROFIT AND REMAINING FUNDS", + withFee: false, + liquidationDiscount: 9600, + feeLiquidation: 150, + feeInterest: 5000, + totalValue: 10000, + debt: 5000, + accruedInterest: 2000, + amountToPool: 8150, + remainingFunds: 1449, + profit: 1150, + loss: 0 + }), + CalcLiquidationPaymentsTestCase({ + name: "NORMAL LIQUIDATION WITH PROFIT AND NO REMAINING FUNDS", + withFee: false, + liquidationDiscount: 9600, + feeLiquidation: 150, + feeInterest: 5000, + totalValue: 10000, + debt: 6500, + accruedInterest: 2000, + amountToPool: 9600, + remainingFunds: 0, + profit: 1100, + loss: 0 + }), + CalcLiquidationPaymentsTestCase({ + name: "NORMAL LIQUIDATION WITH LOSS", + withFee: false, + liquidationDiscount: 9600, + feeLiquidation: 150, + feeInterest: 5000, + totalValue: 10000, + debt: 7000, + accruedInterest: 3000, + amountToPool: 9600, + remainingFunds: 0, + profit: 0, + loss: 400 + }), + CalcLiquidationPaymentsTestCase({ + name: "EXPIRED LIQUIDATION WITH PROFIT AND REMAINING FUNDS", + withFee: false, + liquidationDiscount: 9800, + feeLiquidation: 100, + feeInterest: 5000, + totalValue: 10000, + debt: 5000, + accruedInterest: 2000, + amountToPool: 8100, + remainingFunds: 1699, + profit: 1100, + loss: 0 + }), + CalcLiquidationPaymentsTestCase({ + name: "EXPIRED LIQUIDATION WITH PROFIT AND NO REMAINING FUNDS", + withFee: false, + liquidationDiscount: 9800, + feeLiquidation: 100, + feeInterest: 5000, + totalValue: 10000, + debt: 6800, + accruedInterest: 2000, + amountToPool: 9800, + remainingFunds: 0, + profit: 1000, + loss: 0 + }), + CalcLiquidationPaymentsTestCase({ + name: "EXPIRED LIQUIDATION WITH LOSS", + withFee: false, + liquidationDiscount: 9800, + feeLiquidation: 100, + feeInterest: 5000, + totalValue: 10000, + debt: 7000, + accruedInterest: 3000, + amountToPool: 9800, + remainingFunds: 0, + profit: 0, + loss: 200 + }), + CalcLiquidationPaymentsTestCase({ + name: "NORMAL LIQUIDATION WITH PROFIT AND REMAINING FUNDS + FEE", + withFee: true, + liquidationDiscount: 9600, + feeLiquidation: 150, + feeInterest: 5000, + totalValue: 10000, + debt: 5000, + accruedInterest: 2000, + amountToPool: 8190, + remainingFunds: 1409, + profit: 1150, + loss: 0 + }), + CalcLiquidationPaymentsTestCase({ + name: "NORMAL LIQUIDATION WITH PROFIT AND NO REMAINING FUNDS + FEE", + withFee: true, + liquidationDiscount: 9600, + feeLiquidation: 150, + feeInterest: 5000, + totalValue: 10000, + debt: 6500, + accruedInterest: 2000, + amountToPool: 9600, + remainingFunds: 0, + profit: 1052, + loss: 0 + }), + CalcLiquidationPaymentsTestCase({ + name: "NORMAL LIQUIDATION WITH LOSS + FEE", + withFee: true, + liquidationDiscount: 9600, + feeLiquidation: 150, + feeInterest: 5000, + totalValue: 10000, + debt: 7000, + accruedInterest: 3000, + amountToPool: 9600, + remainingFunds: 0, + profit: 0, + loss: 448 + }) + ]; + + for (uint256 i = 0; i < cases.length; i++) { + CollateralDebtData memory cdd; + + cdd.totalValue = cases[i].totalValue; + cdd.debt = cases[i].debt; + cdd.accruedInterest = cases[i].accruedInterest; + cdd.accruedFees = cases[i].accruedInterest * cases[i].feeInterest / PERCENTAGE_FACTOR; + + (uint256 amountToPool, uint256 remainingFunds, uint256 profit, uint256 loss) = CreditLogic + .calcLiquidationPayments( + cdd, + cases[i].feeLiquidation, + cases[i].liquidationDiscount, + cases[i].withFee ? _amountPlusFee : _amountWithoutFee, + cases[i].withFee ? _amountMinusFee : _amountWithoutFee + ); + + assertEq(amountToPool, cases[i].amountToPool, string(abi.encodePacked(cases[i].name, ": amountToPool"))); + assertEq( + remainingFunds, cases[i].remainingFunds, string(abi.encodePacked(cases[i].name, ": remainingFunds")) + ); + assertEq(profit, cases[i].profit, string(abi.encodePacked(cases[i].name, ": profit"))); + assertEq(loss, cases[i].loss, string(abi.encodePacked(cases[i].name, ": loss"))); + } + } + + struct LiquidationThresholdTestCase { + string name; + uint16 ltInitial; + uint16 ltFinal; + uint40 timestampRampStart; + uint24 rampDuration; + uint16 expectedLT; + } + + /// @notice U:[CL-5]: `calcLiquidationThreshold` gives expected outputs + function test_U_CL_05_getLiquidationThreshold_case_test() public { + LiquidationThresholdTestCase[6] memory cases = [ + LiquidationThresholdTestCase({ + name: "LIQUIDATION THRESHOLD RAMP IN THE FUTURE", + ltInitial: 4000, + ltFinal: 6000, + timestampRampStart: uint40(block.timestamp + 1000), + rampDuration: 3600, + expectedLT: 4000 + }), + LiquidationThresholdTestCase({ + name: "LIQUIDATION THRESHOLD RAMP IN THE PAST", + ltInitial: 4000, + ltFinal: 6000, + timestampRampStart: uint40(block.timestamp - 10000), + rampDuration: 3600, + expectedLT: 6000 + }), + LiquidationThresholdTestCase({ + name: "LIQUIDATION THRESHOLD RAMP ONE-THIRD WAY ASCENDING", + ltInitial: 3000, + ltFinal: 6000, + timestampRampStart: uint40(block.timestamp - 5000), + rampDuration: 15000, + expectedLT: 4000 + }), + LiquidationThresholdTestCase({ + name: "LIQUIDATION THRESHOLD RAMP ONE-HALF WAY ASCENDING", + ltInitial: 4500, + ltFinal: 5000, + timestampRampStart: uint40(block.timestamp - 7500), + rampDuration: 15000, + expectedLT: 4750 + }), + LiquidationThresholdTestCase({ + name: "LIQUIDATION THRESHOLD RAMP ONE-THIRD WAY DESCENDING", + ltInitial: 2000, + ltFinal: 1000, + timestampRampStart: uint40(block.timestamp - 5000), + rampDuration: 15000, + expectedLT: 1666 + }), + LiquidationThresholdTestCase({ + name: "LIQUIDATION THRESHOLD RAMP ONE-HALF WAY DESCENDING", + ltInitial: 9000, + ltFinal: 8900, + timestampRampStart: uint40(block.timestamp - 7500), + rampDuration: 15000, + expectedLT: 8950 + }) + ]; + + for (uint256 i = 0; i < cases.length; i++) { + ctd.ltInitial = cases[i].ltInitial; + ctd.ltFinal = cases[i].ltFinal; + ctd.timestampRampStart = cases[i].timestampRampStart; + ctd.rampDuration = cases[i].rampDuration; + + assertEq( + CreditLogic.getLiquidationThreshold(ctd), + cases[i].expectedLT, + string(abi.encodePacked(cases[i].name, ": LT")) + ); + } + } + + function _collateralTokenByMask(uint256 mask, bool computeLT) internal view returns (address token, uint16 lt) { + for (uint256 i = 0; i < 8; ++i) { + if (mask == (1 << i)) { + token = tokens[i]; + lt = computeLT ? tokenLTsStorage[i] : 0; + } + } + + if (token == address(0)) { + revert("Token not found"); + } + } + + function _convertToUSD(address, uint256 amount, address token) internal view returns (uint256) { + uint256 tokenIdx; + for (uint256 i = 0; i < 8; ++i) { + if (tokens[i] == token) tokenIdx = i; + } + return tokenPricesStorage[tokenIdx] * tokenBalancesStorage[tokenIdx] / WAD; + } + + // /// @notice U:[CL-6]: `calcQuotedTokensCollateral` fuzzing test + // function test_CL_06_calcQuotedTokensCollateral_fuzz_test( + // uint256[8] memory tokenBalances, + // uint256[8] memory tokenPrices, + // uint256[8] memory tokenQuotas, + // uint256 limit, + // uint16[8] memory lts + // ) public { + // _prepareTokens(); + + // CollateralDebtData memory cdd; + + // address creditAccount = makeAddr("CREDIT_ACCOUNT"); + // address underlying = makeAddr("UNDERLYING"); + + // for (uint256 i = 0; i < 8; ++i) { + // tokenBalances[i] = tokenBalances[i] % (WAD * 10 ** 9); + // tokenQuotas[i] = tokenQuotas[i] % (WAD * 10 ** 9); + // lts[i] = lts[i] % 9451; + // tokenPrices[i] = 10 ** 5 + tokenPrices[i] % (100000 * 10 ** 8); + + // emit log_string("TOKEN"); + // emit log_uint(i); + // emit log_string("BALANCE"); + // emit log_uint(tokenBalances[i]); + // emit log_string("QUOTA"); + // emit log_uint(tokenQuotas[i]); + // emit log_string("LT"); + // emit log_uint(lts[i]); + // emit log_string("TOKEN PRICE"); + // emit log_uint(tokenPrices[i]); + + // vm.mockCall(tokens[i], abi.encodeCall(IERC20.balanceOf, (creditAccount)), abi.encode(tokenBalances[i])); + // } + + // tokenBalancesStorage = tokenBalances; + // tokenPricesStorage = tokenPrices; + // tokenLTsStorage = lts; + + // cdd.quotedTokens = _getTokenArray(); + // cdd.quotedLts = _getLTArray(); + + // { + // uint256[] memory quotas = new uint256[](8); + + // for (uint256 i = 0; i < 8; ++i) { + // quotas[i] = tokenQuotas[i]; + // } + + // cdd.quotas = quotas; + // } + + // (cdd.totalValueUSD, cdd.twvUSD) = CreditLogic.calcQuotedTokensCollateral( + // cdd, creditAccount, 10 ** 8 * RAY / WAD, limit, _convertToUSD, address(0) + // ); + + // uint256 twvExpected; + // uint256 totalValueExpected; + // uint256 interestExpected; + + // for (uint256 i = 0; i < 8; ++i) { + // uint256 balanceValue = tokenBalances[i] * tokenPrices[i] / WAD; + // uint256 quotaValue = tokenQuotas[i] / 10 ** 10; + // totalValueExpected += balanceValue; + // twvExpected += (balanceValue < quotaValue ? balanceValue : quotaValue) * lts[i] / PERCENTAGE_FACTOR; + + // if (twvExpected >= limit) break; + // } + + // assertLe(_calcDiff(cdd.twvUSD, twvExpected), 1, "Incorrect twv"); + + // assertEq(cdd.totalValueUSD, totalValueExpected, "Incorrect total value"); + // } + + // /// @notice U:[CL-7]: `calcNonQuotedTokensCollateral` fuzzing test + // function test_CL_07_calcNonQuotedTokensCollateral_fuzz_test( + // uint256 collateralHintsRand, + // uint256 tokensToCheck, + // uint256[8] memory tokenBalances, + // uint256[8] memory tokenPrices, + // uint256 limit, + // uint16[8] memory lts + // ) public { + // _prepareTokens(); + + // tokensToCheck %= 2 ** 8; + + // emit log_string("LIMIT"); + // emit log_uint(limit); + + // for (uint256 i = 0; i < 8; ++i) { + // tokenBalances[i] = tokenBalances[i] % (WAD * 10 ** 9); + // lts[i] = lts[i] % 9451; + // tokenPrices[i] = 10 ** 5 + tokenPrices[i] % (100000 * 10 ** 8); + + // emit log_string("TOKEN"); + // emit log_uint(i); + // emit log_string("BALANCE"); + // emit log_uint(tokenBalances[i]); + // emit log_string("LT"); + // emit log_uint(lts[i]); + // emit log_string("TOKEN PRICE"); + // emit log_uint(tokenPrices[i]); + // emit log_string("CHECKED"); + // emit log_uint(tokensToCheck & (1 << i) == 0 ? 0 : 1); + + // vm.mockCall( + // tokens[i], abi.encodeCall(IERC20.balanceOf, (makeAddr("CREDIT_ACCOUNT"))), abi.encode(tokenBalances[i]) + // ); + // } + + // uint256[] memory colHints = _getCollateralHintsIdx(collateralHintsRand); + + // emit log_string("COLLATERAL HINTS"); + // for (uint256 i = 0; i < colHints.length; ++i) { + // emit log_uint(colHints[i]); + // } + + // tokenBalancesStorage = tokenBalances; + // tokenPricesStorage = tokenPrices; + // tokenLTsStorage = lts; + + // (uint256 totalValueUSD, uint256 twvUSD, uint256 tokensToDisable) = CreditLogic.calcNonQuotedTokensCollateral( + // makeAddr("CREDIT_ACCOUNT"), + // limit, + // _getMasksFromIdx(colHints), + // _convertToUSD, + // _collateralTokenByMask, + // tokensToCheck, + // address(0) + // ); + + // uint256 twvExpected; + // uint256 totalValueExpected; + // uint256 tokensToDisableExpected; + + // for (uint256 i = 0; i < colHints.length; ++i) { + // uint256 idx = colHints[i]; + + // if (tokensToCheck & (1 << idx) != 0) { + // if (tokenBalances[idx] > 1) { + // uint256 balanceValue = tokenBalances[idx] * tokenPrices[idx] / WAD; + // totalValueExpected += balanceValue; + // twvExpected += balanceValue * lts[idx] / PERCENTAGE_FACTOR; + + // if (twvExpected >= limit) break; + // } else { + // tokensToDisableExpected += 1 << idx; + // } + // } + + // tokensToCheck = tokensToCheck & ~(1 << idx); + // } + + // for (uint256 i = 0; i < 8; ++i) { + // if (tokensToCheck & (1 << i) != 0) { + // if (tokenBalances[i] > 1) { + // uint256 balanceValue = tokenBalances[i] * tokenPrices[i] / WAD; + // totalValueExpected += balanceValue; + // twvExpected += balanceValue * lts[i] / PERCENTAGE_FACTOR; + + // if (twvExpected >= limit) break; + // } else { + // tokensToDisableExpected += 1 << i; + // } + // } + // } + + // assertLe(_calcDiff(twvUSD, twvExpected), 1, "Incorrect twv"); + + // assertEq(totalValueUSD, totalValueExpected, "Incorrect total value"); + + // assertEq(tokensToDisable, tokensToDisableExpected, "Incorrect tokens to disable"); + // } } diff --git a/contracts/test/unit/libraries/WithdrawalsLogic.t.sol b/contracts/test/unit/libraries/WithdrawalsLogic.t.sol index 5e66ea98..220f06df 100644 --- a/contracts/test/unit/libraries/WithdrawalsLogic.t.sol +++ b/contracts/test/unit/libraries/WithdrawalsLogic.t.sol @@ -14,9 +14,9 @@ enum ScheduleTask { NON_SCHEDULED } -/// @title Withdrawals logic test -/// @notice [WL]: Unit tests for withdrawals library -contract WithdrawalsLogicTest is TestHelper { +/// @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]; @@ -27,16 +27,16 @@ contract WithdrawalsLogicTest is TestHelper { uint8 constant TOKEN_INDEX = 1; uint256 constant AMOUNT = 1 ether; - /// @notice [WL-1]: `clear` works correctly - function test_WL_01_clear_works_correctly() public { + /// @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 [WL-2]: `tokenMaskAndAmount` works correctly - function test_WL_02_tokenMaskAndAmount_works_correctly() public { + /// @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)); @@ -66,8 +66,8 @@ contract WithdrawalsLogicTest is TestHelper { uint8 expectedSlot; } - /// @notice [WL-3]: `findFreeSlot` works correctly - function test_WL_03_findFreeSlot_works_correctly() public { + /// @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", @@ -118,8 +118,8 @@ contract WithdrawalsLogicTest is TestHelper { bool expectedResult; } - /// @notice [WL-4]: `claimAllowed` works correctly - function test_WL_04_claimAllowed_works_correctly() public { + /// @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", @@ -205,8 +205,8 @@ contract WithdrawalsLogicTest is TestHelper { } } - /// @notice [WL-5]: `cancelAllowed` works correctly - function test_WL_05_cancelAllowed_works_correctly() public { + /// @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", diff --git a/contracts/test/unit/pool/LinearInterestRateModel.t.sol b/contracts/test/unit/pool/LinearInterestRateModel.unit.t.sol similarity index 95% rename from contracts/test/unit/pool/LinearInterestRateModel.t.sol rename to contracts/test/unit/pool/LinearInterestRateModel.unit.t.sol index 6f88e114..495d0de7 100644 --- a/contracts/test/unit/pool/LinearInterestRateModel.t.sol +++ b/contracts/test/unit/pool/LinearInterestRateModel.unit.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IInterestRateModel} from "../../../interfaces/IInterestRateModel.sol"; import {LinearInterestRateModel} from "../../../pool/LinearInterestRateModel.sol"; @@ -19,21 +19,19 @@ import "../../../interfaces/IExceptions.sol"; import {TestHelper} from "../../lib/helper.sol"; import "forge-std/console.sol"; -/// @title pool -/// @notice Business logic for borrowing liquidity pools -contract LinearInterestRateModelTest is TestHelper { +contract LinearInterestRateModelUniTest is TestHelper { using Math for uint256; LinearInterestRateModel irm; function setUp() public { irm = new LinearInterestRateModel( - 8000, - 9500, - 1000, - 2000, - 3000, - 4000, + 80_00, + 95_00, + 10_00, + 20_00, + 30_00, + 40_00, true ); } @@ -42,8 +40,8 @@ contract LinearInterestRateModelTest is TestHelper { // TESTS // - // [LIM-1]: start parameters are correct - function test_LIM_01_start_parameters_correct() public { + // U:[LIM-1]: start parameters are correct + function test_U_LIM_01_start_parameters_correct() public { (uint16 U_1, uint16 U_2, uint16 R_base, uint16 R_slope1, uint16 R_slope2, uint16 R_slope3) = irm.getModelParameters(); @@ -67,8 +65,8 @@ contract LinearInterestRateModelTest is TestHelper { uint16 R_slope3; } - // [LIM-2]: linear model constructor reverts for incorrect params - function test_LIM_02_linear_model_constructor_reverts_for_incorrect_params() public { + // U:[LIM-2]: linear model constructor reverts for incorrect params + function test_U_LIM_02_linear_model_constructor_reverts_for_incorrect_params() public { // adds liqudity to mint initial diesel tokens to change 1:1 rate IncorrectParamCase[8] memory cases = [ @@ -189,8 +187,8 @@ contract LinearInterestRateModelTest is TestHelper { bool expectedRevert; } - // [LIM-3]: linear model computes available to borrow and borrow rate correctly - function test_LIM_03_linear_model_computes_available_to_borrow_and_borrow_rate_correctly() public { + // U:[LIM-3]: linear model computes available to borrow and borrow rate correctly + function test_U_LIM_03_linear_model_computes_available_to_borrow_and_borrow_rate_correctly() public { // adds liqudity to mint initial diesel tokens to change 1:1 rate LinearCalculationsCase[12] memory cases = [ diff --git a/contracts/test/unit/pool/PoolQuotaKeeper.t.sol b/contracts/test/unit/pool/PoolQuotaKeeper.unit.t.sol similarity index 75% rename from contracts/test/unit/pool/PoolQuotaKeeper.t.sol rename to contracts/test/unit/pool/PoolQuotaKeeper.unit.t.sol index 8daca4f9..a6546ce6 100644 --- a/contracts/test/unit/pool/PoolQuotaKeeper.t.sol +++ b/contracts/test/unit/pool/PoolQuotaKeeper.unit.t.sol @@ -1,21 +1,24 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../../../interfaces/IAddressProviderV3.sol"; +import {AddressProviderV3ACLMock} from "../../mocks/core/AddressProviderV3ACLMock.sol"; +import {ContractsRegister} from "@gearbox-protocol/core-v2/contracts/core/ContractsRegister.sol"; + import {IPoolQuotaKeeper, IPoolQuotaKeeperEvents, TokenQuotaParams} from "../../../interfaces/IPoolQuotaKeeper.sol"; import {IGauge} from "../../../interfaces/IGauge.sol"; -import {IPool4626} from "../../../interfaces/IPool4626.sol"; +import {IPoolV3} from "../../../interfaces/IPoolV3.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {PoolServiceMock} from "../../mocks/pool/PoolServiceMock.sol"; +import {PoolMock} from "../../mocks/pool/PoolMock.sol"; import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; -import {CreditManagerMockForPoolTest} from "../../mocks/pool/CreditManagerMockForPoolTest.sol"; -import {addLiquidity, referral, PoolQuotaKeeperTestSuite} from "../../suites/PoolQuotaKeeperTestSuite.sol"; +import {CreditManagerMock} from "../../mocks/credit/CreditManagerMock.sol"; import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; import {Tokens} from "../../config/Tokens.sol"; @@ -35,19 +38,18 @@ import "../../../interfaces/IExceptions.sol"; import {TestHelper} from "../../lib/helper.sol"; import "forge-std/console.sol"; -/// @title pool -/// @notice Business logic for borrowing liquidity pools -contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvents { +contract PoolQuotaKeeperUnitTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvents { using Math for uint256; - PoolQuotaKeeperTestSuite psts; + ContractsRegister public cr; + PoolQuotaKeeper pqk; GaugeMock gaugeMock; - ACL acl; - PoolServiceMock pool; + PoolMock pool; address underlying; - CreditManagerMockForPoolTest cmMock; + + CreditManagerMock cmMock; function setUp() public { _setUp(Tokens.DAI); @@ -55,32 +57,50 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent function _setUp(Tokens t) public { tokenTestSuite = new TokensTestSuite(); - psts = new PoolQuotaKeeperTestSuite( - tokenTestSuite, - tokenTestSuite.addressOf(t) - ); - - pool = psts.pool4626(); - - underlying = address(psts.underlying()); - cmMock = psts.cmMock(); - acl = psts.acl(); - pqk = psts.poolQuotaKeeper(); - gaugeMock = psts.gaugeMock(); + + tokenTestSuite.topUpWETH{value: 100 * WAD}(); + + underlying = tokenTestSuite.addressOf(t); + + AddressProviderV3ACLMock addressProvider = new AddressProviderV3ACLMock(); + addressProvider.setAddress(AP_WETH_TOKEN, tokenTestSuite.addressOf(Tokens.WETH), false); + + pool = new PoolMock(address(addressProvider), underlying); + + pqk = new PoolQuotaKeeper(address(pool)); + + pool.setPoolQuotaManager(address(pqk)); + + gaugeMock = new GaugeMock(address(pool)); + + pqk.setGauge(address(gaugeMock)); + + vm.startPrank(CONFIGURATOR); + + cmMock = new CreditManagerMock(address(addressProvider), address(pool)); + + cr = ContractsRegister(addressProvider.getAddressOrRevert(AP_CONTRACTS_REGISTER, 1)); + + cr.addPool(address(pool)); + cr.addCreditManager(address(cmMock)); + + vm.label(address(pool), "Pool"); + + vm.stopPrank(); } // // TESTS // - // [PQK-1]: constructor sets parameters correctly - function test_PQK_01_constructor_sets_parameters_correctly() public { - assertEq(address(pool), address(pqk.pool()), "Incorrect pool address"); + // U:[PQK-1]: constructor sets parameters correctly + function test_U_PQK_01_constructor_sets_parameters_correctly() public { + assertEq(address(pool), pqk.pool(), "Incorrect pool address"); assertEq(underlying, pqk.underlying(), "Incorrect pool address"); } - // [PQK-2]: configuration functions revert if called nonConfigurator(nonController) - function test_PQK_02_configuration_functions_reverts_if_call_nonConfigurator() public { + // U:[PQK-2]: configuration functions revert if called nonConfigurator(nonController) + function test_U_PQK_02_configuration_functions_reverts_if_call_nonConfigurator() public { vm.startPrank(USER); vm.expectRevert(CallerNotConfiguratorException.selector); @@ -95,8 +115,8 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent vm.stopPrank(); } - // [PQK-3]: gaugeOnly funcitons revert if called by non-gauge contract - function test_PQK_03_gaugeOnly_funcitons_reverts_if_called_by_non_gauge() public { + // U:[PQK-3]: gaugeOnly funcitons revert if called by non-gauge contract + function test_U_PQK_03_gaugeOnly_funcitons_reverts_if_called_by_non_gauge() public { vm.startPrank(USER); vm.expectRevert(CallerNotGaugeException.selector); @@ -108,8 +128,8 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent vm.stopPrank(); } - // [PQK-4]: creditManagerOnly funcitons revert if called by non registered creditManager - function test_PQK_04_gaugeOnly_funcitons_reverts_if_called_by_non_gauge() public { + // U:[PQK-4]: creditManagerOnly funcitons revert if called by non registered creditManager + function test_U_PQK_04_gaugeOnly_funcitons_reverts_if_called_by_non_gauge() public { vm.startPrank(USER); vm.expectRevert(CallerNotCreditManagerException.selector); @@ -123,8 +143,8 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent vm.stopPrank(); } - // [PQK-5]: addQuotaToken adds token and set parameters correctly - function test_PQK_05_addQuotaToken_adds_token_and_set_parameters_correctly() public { + // U:[PQK-5]: addQuotaToken adds token and set parameters correctly + function test_U_PQK_05_addQuotaToken_adds_token_and_set_parameters_correctly() public { address[] memory tokens = pqk.quotedTokens(); assertEq(tokens.length, 0, "SETUP: tokens set unexpectedly has tokens"); @@ -150,8 +170,8 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent assertEq(cumulativeIndexLU_RAY, RAY, "Cumulative index !=RAY"); } - // [PQK-6]: addQuotaToken reverts on adding the same token twice - function test_PQK_06_addQuotaToken_reverts_on_adding_the_same_token_twice() public { + // U:[PQK-6]: addQuotaToken reverts on adding the same token twice + function test_U_PQK_06_addQuotaToken_reverts_on_adding_the_same_token_twice() public { address gauge = pqk.gauge(); vm.prank(gauge); pqk.addQuotaToken(DUMB_ADDRESS); @@ -161,8 +181,8 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent pqk.addQuotaToken(DUMB_ADDRESS); } - // [PQK-7]: updateRates works as expected - function test_PQK_07_updateRates_works_as_expected() public { + // U:[PQK-7]: updateRates works as expected + function test_U_PQK_07_updateRates_works_as_expected() public { address DAI = tokenTestSuite.addressOf(Tokens.DAI); address USDC = tokenTestSuite.addressOf(Tokens.USDC); @@ -170,35 +190,31 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent uint16 USDC_QUOTA_RATE = 45_00; for (uint256 caseIndex; caseIndex < 2; ++caseIndex) { - string memory caseName = caseIndex == 1 ? "With totalQuoted" : "Without totalQuotae"; + caseName = caseIndex == 1 ? "With totalQuoted" : "Without totalQuotae"; setUp(); - vm.prank(CONFIGURATOR); + gaugeMock.addQuotaToken(DAI, DAI_QUOTA_RATE); - vm.prank(CONFIGURATOR); gaugeMock.addQuotaToken(USDC, USDC_QUOTA_RATE); int96 daiQuota; int96 usdcQuota; if (caseIndex == 1) { - vm.startPrank(CONFIGURATOR); pqk.addCreditManager(address(cmMock)); pqk.setTokenLimit(DAI, uint96(100_000 * WAD)); pqk.setTokenLimit(USDC, uint96(100_000 * WAD)); - cmMock.addToken(DAI, 1); - cmMock.addToken(USDC, 2); - - vm.stopPrank(); - daiQuota = int96(uint96(100 * WAD)); usdcQuota = int96(uint96(200 * WAD)); - cmMock.updateQuota({_creditAccount: DUMB_ADDRESS, token: DAI, quotaChange: daiQuota}); - cmMock.updateQuota({_creditAccount: DUMB_ADDRESS, token: USDC, quotaChange: usdcQuota}); + vm.prank(address(cmMock)); + pqk.updateQuota({creditAccount: DUMB_ADDRESS, token: DAI, quotaChange: daiQuota}); + + vm.prank(address(cmMock)); + pqk.updateQuota({creditAccount: DUMB_ADDRESS, token: USDC, quotaChange: usdcQuota}); } vm.warp(block.timestamp + 365 days); @@ -216,44 +232,38 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent uint96 expectedQuotaRevenue = uint96(DAI_QUOTA_RATE * uint96(daiQuota) + USDC_QUOTA_RATE * uint96(usdcQuota)); - vm.expectCall(address(pool), abi.encodeCall(IPool4626.updateQuotaRevenue, expectedQuotaRevenue)); + vm.expectCall(address(pool), abi.encodeCall(IPoolV3.updateQuotaRevenue, expectedQuotaRevenue)); gaugeMock.updateEpoch(); (uint96 totalQuoted, uint96 limit, uint16 rate, uint192 cumulativeIndexLU_RAY) = pqk.totalQuotaParams(DAI); - assertEq(rate, DAI_QUOTA_RATE, _testCaseErr(caseName, "Incorrect DAI rate")); + assertEq(rate, DAI_QUOTA_RATE, _testCaseErr("Incorrect DAI rate")); assertEq( cumulativeIndexLU_RAY, RAY * (PERCENTAGE_FACTOR + DAI_QUOTA_RATE) / PERCENTAGE_FACTOR, - _testCaseErr(caseName, "Incorrect DAI cumulativeIndexLU") + _testCaseErr("Incorrect DAI cumulativeIndexLU") ); (totalQuoted, limit, rate, cumulativeIndexLU_RAY) = pqk.totalQuotaParams(USDC); - assertEq(rate, USDC_QUOTA_RATE, _testCaseErr(caseName, "Incorrect USDC rate")); + assertEq(rate, USDC_QUOTA_RATE, _testCaseErr("Incorrect USDC rate")); assertEq( cumulativeIndexLU_RAY, RAY * (PERCENTAGE_FACTOR + USDC_QUOTA_RATE) / PERCENTAGE_FACTOR, - _testCaseErr(caseName, "Incorrect USDC cumulativeIndexLU") + _testCaseErr("Incorrect USDC cumulativeIndexLU") ); - assertEq( - pqk.lastQuotaRateUpdate(), - block.timestamp, - _testCaseErr(caseName, "Incorect lastQuotaRateUpdate timestamp") - ); + assertEq(pqk.lastQuotaRateUpdate(), block.timestamp, _testCaseErr("Incorect lastQuotaRateUpdate timestamp")); - assertEq(pool.quotaRevenue(), expectedQuotaRevenue, _testCaseErr(caseName, "Incorect expectedQuotaRevenue")); + assertEq(pool.quotaRevenue(), expectedQuotaRevenue, _testCaseErr("Incorect expectedQuotaRevenue")); } } - // [PQK-8]: setGauge works as expected - function test_PQK_08_setGauge_works_as_expected() public { + // U:[PQK-8]: setGauge works as expected + function test_U_PQK_08_setGauge_works_as_expected() public { pqk = new PoolQuotaKeeper(address(pool)); - vm.startPrank(CONFIGURATOR); - assertEq(pqk.gauge(), address(0), "SETUP: incorrect address at start"); vm.warp(block.timestamp + 2 days); @@ -273,27 +283,22 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent // IF address the same, the function updates nothing pqk.setGauge(address(gaugeMock)); assertEq(pqk.lastQuotaRateUpdate(), gaugeUpdateTimestamp, "lastQuotaRateUpdate was unexpectedly updated"); - - vm.stopPrank(); } - // [PQK-9]: addCreditManager works as expected - function test_PQK_09_addCreditManager_reverts_for_non_cm_contract() public { - vm.prank(CONFIGURATOR); - + // U:[PQK-9]: addCreditManager works as expected + function test_U_PQK_09_addCreditManager_reverts_for_non_cm_contract() public { vm.expectRevert(RegisteredCreditManagerOnlyException.selector); pqk.addCreditManager(DUMB_ADDRESS); - cmMock.changePoolService(DUMB_ADDRESS); + cmMock.setPoolService(DUMB_ADDRESS); vm.expectRevert(IncompatibleCreditManagerException.selector); - vm.prank(CONFIGURATOR); pqk.addCreditManager(address(cmMock)); } - // [PQK-10]: addCreditManager works as expected - function test_PQK_10_addCreditManager_works_as_expected() public { + // U:[PQK-10]: addCreditManager works as expected + function test_U_PQK_10_addCreditManager_works_as_expected() public { pqk = new PoolQuotaKeeper(address(pool)); address[] memory managers = pqk.creditManagers(); @@ -303,7 +308,6 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent vm.expectEmit(true, true, false, false); emit AddCreditManager(address(cmMock)); - vm.prank(CONFIGURATOR); pqk.addCreditManager(address(cmMock)); managers = pqk.creditManagers(); @@ -311,7 +315,6 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent assertEq(managers[0], address(cmMock), "Incorrect address was added to creditManagerSet"); // check that funciton works correctly for another one step - vm.prank(CONFIGURATOR); pqk.addCreditManager(address(cmMock)); managers = pqk.creditManagers(); @@ -319,23 +322,18 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent assertEq(managers[0], address(cmMock), "Incorrect address was added to creditManagerSet"); } - // [PQK-11]: setTokenLimit reverts for unregistered token - function test_PQK_11_reverts_for_unregistered_token() public { + // U:[PQK-11]: setTokenLimit reverts for unregistered token + function test_U_PQK_11_reverts_for_unregistered_token() public { vm.expectRevert(TokenIsNotQuotedException.selector); - vm.prank(CONFIGURATOR); - pqk.setTokenLimit(DUMB_ADDRESS, 1); } - // [PQK-12]: setTokenLimit works as expected - function test_PQK_12_setTokenLimit_works_as_expected() public { + // U:[PQK-12]: setTokenLimit works as expected + function test_U_PQK_12_setTokenLimit_works_as_expected() public { uint96 limit = 435_223_999; - vm.prank(CONFIGURATOR); gaugeMock.addQuotaToken(DUMB_ADDRESS, 11); - vm.prank(CONFIGURATOR); - vm.expectEmit(true, true, false, true); emit SetTokenLimit(DUMB_ADDRESS, limit); @@ -346,17 +344,15 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent assertEq(limitSet, limit, "Incorrect limit was set"); } - // [PQK-13]: updateQuotas reverts for unregistered token - function test_PQK_13_updateQuotas_reverts_for_unregistered_token() public { - vm.prank(CONFIGURATOR); + // U:[PQK-13]: updateQuota reverts for unregistered token + function test_U_PQK_13_updateQuotas_reverts_for_unregistered_token() public { pqk.addCreditManager(address(cmMock)); + address link = tokenTestSuite.addressOf(Tokens.LINK); vm.expectRevert(TokenIsNotQuotedException.selector); - cmMock.updateQuota({ - _creditAccount: DUMB_ADDRESS, - token: tokenTestSuite.addressOf(Tokens.LINK), - quotaChange: int96(uint96(100 * WAD)) - }); + + vm.prank(address(cmMock)); + pqk.updateQuota({creditAccount: DUMB_ADDRESS, token: link, quotaChange: int96(uint96(100 * WAD))}); } struct QuotaTest { @@ -391,8 +387,8 @@ contract PoolQuotaKeeperTest is TestHelper, BalanceHelper, IPoolQuotaKeeperEvent uint256 expectedInAYearEnableTokenMaskUpdated; } - // // [PQK-14]: updateQuotas works as expected - // function test_PQK_14_updateQuotas_works_as_expected() public { + // // U:[PQK-14]: updateQuotas works as expected + // function test_U_PQK_14_updateQuotas_works_as_expected() public { // UpdateQuotasTestCase[1] memory cases = [ // UpdateQuotasTestCase({ // name: "Quota simple test", diff --git a/contracts/test/unit/pool/Pool4626.t.sol b/contracts/test/unit/pool/PoolV3.t.sol similarity index 86% rename from contracts/test/unit/pool/Pool4626.t.sol rename to contracts/test/unit/pool/PoolV3.t.sol index fcadd129..91b4ea7b 100644 --- a/contracts/test/unit/pool/Pool4626.t.sol +++ b/contracts/test/unit/pool/PoolV3.t.sol @@ -1,7 +1,11 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; + +import "../../../interfaces/IAddressProviderV3.sol"; +import {AddressProviderV3ACLMock} from "../../mocks/core/AddressProviderV3ACLMock.sol"; +import {ContractsRegister} from "@gearbox-protocol/core-v2/contracts/core/ContractsRegister.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; @@ -11,27 +15,23 @@ import {LinearInterestRateModel} from "../../../pool/LinearInterestRateModel.sol import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {Pool4626} from "../../../pool/Pool4626.sol"; -import {IPool4626Events} from "../../../interfaces/IPool4626.sol"; +import "../../../core/AddressProviderV3.sol"; +import {PoolV3} from "../../../pool/PoolV3.sol"; +import {PoolV3_USDT} from "../../../pool/PoolV3_USDT.sol"; +import {IPoolV3Events} from "../../../interfaces/IPoolV3.sol"; import {IERC4626Events} from "../../interfaces/IERC4626.sol"; import {IInterestRateModel} from "../../../interfaces/IInterestRateModel.sol"; import {ACL} from "@gearbox-protocol/core-v2/contracts/core/ACL.sol"; -import {CreditManagerMockForPoolTest} from "../../mocks/pool/CreditManagerMockForPoolTest.sol"; -import { - liquidityProviderInitBalance, - addLiquidity, - removeLiquidity, - referral, - PoolServiceTestSuite -} from "../../suites/PoolServiceTestSuite.sol"; +import {CreditManagerMock} from "../../mocks/credit/CreditManagerMock.sol"; import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; import {Tokens} from "../../config/Tokens.sol"; import {BalanceHelper} from "../../helpers/BalanceHelper.sol"; import {ERC20FeeMock} from "../../mocks/token/ERC20FeeMock.sol"; import {PoolQuotaKeeper} from "../../../pool/PoolQuotaKeeper.sol"; +import {GaugeMock} from "../../mocks//pool/GaugeMock.sol"; // TEST import {TestHelper} from "../../lib/helper.sol"; @@ -45,28 +45,35 @@ import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/P import "../../../interfaces/IExceptions.sol"; uint256 constant fee = 6000; +uint256 constant liquidityProviderInitBalance = 100 ether; +uint256 constant addLiquidity = 10 ether; +uint256 constant removeLiquidity = 5 ether; +uint16 constant referral = 12333; -/// @title pool -/// @notice Business logic for borrowing liquidity pools -contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Events { +contract PoolV3UnitTest is TestHelper, BalanceHelper, IPoolV3Events, IERC4626Events { using Math for uint256; - PoolServiceTestSuite psts; - PoolQuotaKeeper pqk; + AddressProviderV3ACLMock addressProvider; + ContractsRegister public cr; + + PoolQuotaKeeper public pqk; + GaugeMock public gaugeMock; + + ACL acl; + PoolV3 pool; + address underlying; + CreditManagerMock cmMock; + IInterestRateModel irm; + address treasury; /* * @dev Emitted when `value` tokens are moved from one account (`from`) to * another (`to`). * * Note that `value` may be zero. */ - event Transfer(address indexed from, address indexed to, uint256 value); - ACL acl; - Pool4626 pool; - address underlying; - CreditManagerMockForPoolTest cmMock; - IInterestRateModel irm; + event Transfer(address indexed from, address indexed to, uint256 value); function setUp() public { _setUp(Tokens.DAI, false); @@ -74,19 +81,93 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve function _setUp(Tokens t, bool supportQuotas) public { tokenTestSuite = new TokensTestSuite(); - psts = new PoolServiceTestSuite( - tokenTestSuite, - tokenTestSuite.addressOf(t), - true, - supportQuotas + irm = new LinearInterestRateModel( + 80_00, + 90_00, + 2_00, + 4_00, + 40_00, + 75_00, + false ); - pool = psts.pool4626(); - irm = psts.linearIRModel(); - underlying = address(psts.underlying()); - cmMock = psts.cmMock(); - acl = psts.acl(); - pqk = psts.poolQuotaKeeper(); + vm.startPrank(CONFIGURATOR); + + addressProvider = new AddressProviderV3ACLMock(); + addressProvider.setAddress(AP_WETH_TOKEN, tokenTestSuite.addressOf(Tokens.WETH), false); + + acl = ACL(addressProvider.getAddressOrRevert(AP_ACL, NO_VERSION_CONTROL)); + cr = ContractsRegister(addressProvider.getAddressOrRevert(AP_CONTRACTS_REGISTER, 1)); + treasury = addressProvider.getAddressOrRevert(AP_TREASURY, NO_VERSION_CONTROL); + + underlying = tokenTestSuite.addressOf(t); + + tokenTestSuite.mint(underlying, USER, liquidityProviderInitBalance); + tokenTestSuite.mint(underlying, INITIAL_LP, liquidityProviderInitBalance); + + address newPool; + + bool isFeeToken = false; + + try ERC20FeeMock(underlying).basisPointsRate() returns (uint256) { + isFeeToken = true; + } catch {} + + if (isFeeToken) { + pool = new PoolV3_USDT({ + _addressProvider: address(addressProvider), + _underlyingToken: underlying, + _interestRateModel: address(irm), + _expectedLiquidityLimit: type(uint256).max, + _supportsQuotas: supportQuotas + }); + } else { + pool = new PoolV3({ + _addressProvider: address(addressProvider), + _underlyingToken: underlying, + _interestRateModel: address(irm), + _expectedLiquidityLimit: type(uint256).max, + _supportsQuotas: supportQuotas + }); + } + newPool = address(pool); + + if (supportQuotas) { + _deployAndConnectPoolQuotaKeeper(); + } + + vm.stopPrank(); + + vm.prank(USER); + IERC20(underlying).approve(newPool, type(uint256).max); + + vm.prank(INITIAL_LP); + IERC20(underlying).approve(newPool, type(uint256).max); + + vm.startPrank(CONFIGURATOR); + + cmMock = new CreditManagerMock(address(addressProvider), newPool); + + cr.addPool(newPool); + cr.addCreditManager(address(cmMock)); + + vm.label(newPool, "Pool"); + + // vm.label(address(underlying), "UnderlyingToken"); + + vm.stopPrank(); + } + + function _deployAndConnectPoolQuotaKeeper() internal { + pqk = new PoolQuotaKeeper(address(pool)); + + // vm.prank(CONFIGURATOR); + pool.setPoolQuotaManager(address(pqk)); + + gaugeMock = new GaugeMock(address(pool)); + + // vm.prank(CONFIGURATOR); + pqk.setGauge(address(gaugeMock)); } // @@ -123,7 +204,7 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve } function _borrowToUtilisation(uint16 utilisation) internal { - cmMock.lendCreditAccount(pool.expectedLiquidity() / 2, DUMB_ADDRESS); + cmMock.lendCreditAccount(pool.expectedLiquidity() * utilisation / PERCENTAGE_FACTOR, DUMB_ADDRESS); assertEq(pool.borrowRate(), irm.calcBorrowRate(PERCENTAGE_FACTOR, utilisation, false)); } @@ -162,70 +243,75 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve // TESTS // - // [P4-1]: getDieselRate_RAY=RAY, withdrawFee=0 and expectedLiquidityLimit as expected at start - function test_P4_01_start_parameters_correct() public { + // U:[P4-1]: getDieselRate_RAY=RAY, withdrawFee=0 and expectedLiquidityLimit as expected at start + function test_U_P4_01_start_parameters_correct() public { assertEq(pool.name(), "diesel DAI", "Symbol incorrectly set up"); assertEq(pool.symbol(), "dDAI", "Symbol incorrectly set up"); - assertEq(address(pool.addressProvider()), address(psts.addressProvider()), "Incorrect address provider"); + assertEq(address(pool.addressProvider()), address(addressProvider), "Incorrect address provider"); assertEq(pool.asset(), underlying, "Incorrect underlying provider"); assertEq(pool.underlyingToken(), underlying, "Incorrect underlying provider"); - assertEq(pool.decimals(), IERC20Metadata(address(psts.underlying())).decimals(), "Incorrect decimals"); + assertEq(pool.decimals(), IERC20Metadata(underlying).decimals(), "Incorrect decimals"); - assertEq(pool.treasury(), psts.addressProvider().getTreasuryContract(), "Incorrect treasury"); + assertEq( + pool.treasury(), addressProvider.getAddressOrRevert(AP_TREASURY, NO_VERSION_CONTROL), "Incorrect treasury" + ); assertEq(pool.convertToAssets(RAY), RAY, "Incorrect diesel rate!"); - assertEq(address(pool.interestRateModel()), address(psts.linearIRModel()), "Incorrect interest rate model"); + assertEq(address(pool.interestRateModel()), address(irm), "Incorrect interest rate model"); assertEq(pool.expectedLiquidityLimit(), type(uint256).max); assertEq(pool.totalBorrowedLimit(), type(uint256).max); } - // [P4-2]: constructor reverts for zero addresses - function test_P4_02_constructor_reverts_for_zero_addresses() public { + // U:[P4-2]: constructor reverts for zero addresses + function test_U_P4_02_constructor_reverts_for_zero_addresses() public { + address irmodel = address(irm); + address ap = address(addressProvider); + vm.expectRevert(ZeroAddressException.selector); - new Pool4626({ + new PoolV3({ _addressProvider: address(0), _underlyingToken: underlying, - _interestRateModel: address(psts.linearIRModel()), + _interestRateModel: irmodel, _expectedLiquidityLimit: type(uint128).max, _supportsQuotas: false }); - // opts.addressProvider = address(psts.addressProvider()); + // opts.addressProvider = address(addressProvider); // opts.interestRateModel = address(0); vm.expectRevert(ZeroAddressException.selector); - new Pool4626({ - _addressProvider:address(psts.addressProvider()), + new PoolV3({ + _addressProvider: ap, _underlyingToken: underlying, _interestRateModel: address(0), _expectedLiquidityLimit: type(uint128).max, _supportsQuotas: false }); - // opts.interestRateModel = address(psts.linearIRModel()); + // opts.interestRateModel = address(irm); // opts.underlyingToken = address(0); vm.expectRevert(ZeroAddressException.selector); - new Pool4626({ - _addressProvider: address(psts.addressProvider()), + new PoolV3({ + _addressProvider: ap, _underlyingToken: address(0), - _interestRateModel: address(psts.linearIRModel()), + _interestRateModel: irmodel, _expectedLiquidityLimit: type(uint128).max, _supportsQuotas: false }); } - // [P4-3]: constructor emits events - function test_P4_03_constructor_emits_events() public { + // U:[P4-3]: constructor emits events + function test_U_P4_03_constructor_emits_events() public { uint256 limit = 15890; vm.expectEmit(true, false, false, false); - emit SetInterestRateModel(address(psts.linearIRModel())); + emit SetInterestRateModel(address(irm)); vm.expectEmit(true, false, false, true); emit SetExpectedLiquidityLimit(limit); @@ -233,17 +319,17 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve vm.expectEmit(false, false, false, true); emit SetTotalBorrowedLimit(limit); - new Pool4626({ - _addressProvider: address(psts.addressProvider()), + new PoolV3({ + _addressProvider: address(addressProvider), _underlyingToken: underlying, - _interestRateModel: address(psts.linearIRModel()), + _interestRateModel: address(irm), _expectedLiquidityLimit: limit, _supportsQuotas: false }); } - // [P4-4]: addLiquidity, removeLiquidity, lendCreditAccount, repayCreditAccount reverts if contract is paused - function test_P4_04_cannot_be_used_while_paused() public { + // U:[P4-4]: addLiquidity, removeLiquidity, lendCreditAccount, repayCreditAccount reverts if contract is paused + function test_U_P4_04_cannot_be_used_while_paused() public { vm.startPrank(CONFIGURATOR); acl.addPausableAdmin(CONFIGURATOR); pool.pause(); @@ -292,8 +378,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve uint256 expectedLiquidityAfter; } - // [P4-5]: deposit adds liquidity correctly - function test_P4_05_deposit_adds_liquidity_correctly() public { + // U:[P4-5]: deposit adds liquidity correctly + function test_U_P4_05_deposit_adds_liquidity_correctly() public { // adds liqudity to mint initial diesel tokens to change 1:1 rate DepositTestCase[2] memory cases = [ @@ -415,8 +501,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve uint256 expectedLiquidityAfter; } - // [P4-6]: deposit adds liquidity correctly - function test_P4_06_mint_adds_liquidity_correctly() public { + // U:[P4-6]: deposit adds liquidity correctly + function test_U_P4_06_mint_adds_liquidity_correctly() public { MintTestCase[2] memory cases = [ MintTestCase({ name: "Normal token", @@ -514,8 +600,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve } } - // [P4-7]: deposit and mint if assets more than limit - function test_P4_07_deposit_and_mint_if_assets_more_than_limit() public { + // U:[P4-7]: deposit and mint if assets more than limit + function test_U_P4_07_deposit_and_mint_if_assets_more_than_limit() public { for (uint256 j; j < 2; ++j) { for (uint256 i; i < 2; ++i) { bool feeToken = i == 1; @@ -572,8 +658,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve uint256 expectedTreasury; } - // [P4-8]: deposit and mint if assets more than limit - function test_P4_08_withdraw_works_as_expected() public { + // U:[P4-8]: deposit and mint if assets more than limit + function test_U_P4_08_withdraw_works_as_expected() public { WithdrawTestCase[4] memory cases = [ WithdrawTestCase({ name: "Normal token with 0 withdraw fee", @@ -770,8 +856,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve uint256 expectedTreasury; } - // [P4-9]: deposit and mint if assets more than limit - function test_P4_09_redeem_works_as_expected() public { + // U:[P4-9]: deposit and mint if assets more than limit + function test_U_P4_09_redeem_works_as_expected() public { RedeemTestCase[4] memory cases = [ RedeemTestCase({ name: "Normal token with 0 withdraw fee", @@ -947,8 +1033,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve } } - // [P4-10]: burn works as expected - function test_P4_10_burn_works_as_expected() public { + // U:[P4-10]: burn works as expected + function test_U_P4_10_burn_works_as_expected() public { _setUpTestCase(Tokens.DAI, 0, 50_00, addLiquidity, 2 * RAY, 0, false); vm.prank(USER); @@ -978,8 +1064,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve /// /// LEND CREDIT ACCOUNT - // [P4-11]: lendCreditAccount works as expected - function test_P4_11_lendCreditAccount_works_as_expected() public { + // U:[P4-11]: lendCreditAccount works as expected + function test_U_P4_11_lendCreditAccount_works_as_expected() public { _setUpTestCase(Tokens.DAI, 0, 0, addLiquidity, 2 * RAY, 0, false); address creditAccount = DUMB_ADDRESS; @@ -1014,8 +1100,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve assertEq(pool.creditManagerBorrowed(address(cmMock)), borrowAmount, "Incorrect CM limit"); } - // [P4-12]: lendCreditAccount reverts if it breaches limits - function test_P4_12_lendCreditAccount_reverts_if_breach_limits() public { + // U:[P4-12]: lendCreditAccount reverts if it breaches limits + function test_U_P4_12_lendCreditAccount_reverts_if_breach_limits() public { address creditAccount = DUMB_ADDRESS; _setUpTestCase(Tokens.DAI, 0, 0, addLiquidity, 2 * RAY, 0, false); @@ -1044,8 +1130,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve // REPAY // - // [P4-13]: repayCreditAccount reverts for incorrect credit managers - function test_P4_13_repayCreditAccount_reverts_for_incorrect_credit_managers() public { + // U:[P4-13]: repayCreditAccount reverts for incorrect credit managers + function test_U_P4_13_repayCreditAccount_reverts_for_incorrect_credit_managers() public { _setUpTestCase(Tokens.DAI, 0, 0, addLiquidity, 2 * RAY, 0, false); /// Case for unknown CM @@ -1081,8 +1167,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve uint256 uncoveredLoss; } - // [P4-14]: repayCreditAccount works as expected - function test_P4_14_repayCreditAccount_works_as_expected() public { + // U:[P4-14]: repayCreditAccount works as expected + function test_U_P4_14_repayCreditAccount_works_as_expected() public { address creditAccount = DUMB_ADDRESS; RepayTestCase[5] memory cases = [ RepayTestCase({ @@ -1222,7 +1308,7 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve false ); - address treasury = pool.treasury(); + treasury = pool.treasury(); vm.prank(INITIAL_LP); pool.transfer(treasury, testCase.sharesInTreasury); @@ -1292,8 +1378,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve /// CALC LINEAR CUMULATIVE /// - // [P4-15]: calcLinearCumulative_RAY computes correctly - function test_P4_15_calcLinearCumulative_RAY_correct() public { + // U:[P4-15]: calcLinearCumulative_RAY computes correctly + function test_U_P4_15_calcLinearCumulative_RAY_correct() public { _setUpTestCase(Tokens.DAI, 0, 50_00, addLiquidity, 2 * RAY, 0, false); uint256 timeWarp = 180 days; @@ -1307,8 +1393,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve assertEq(pool.calcLinearCumulative_RAY(), expectedLinearRate, "Index value was not updated correctly"); } - // [P4-16]: updateBorrowRate correctly updates parameters - function test_P4_16_updateBorrowRate_correct() public { + // U:[P4-16]: updateBorrowRate correctly updates parameters + function test_U_P4_16_updateBorrowRate_correct() public { uint256 quotaInterestPerYear = addLiquidity / 4; for (uint256 i; i < 2; ++i) { bool supportQuotas = i == 1; @@ -1318,7 +1404,7 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve if (supportQuotas) { vm.startPrank(CONFIGURATOR); - psts.gaugeMock().addQuotaToken(tokenTestSuite.addressOf(Tokens.LINK), 100_00); + gaugeMock.addQuotaToken(tokenTestSuite.addressOf(Tokens.LINK), 100_00); pqk.addCreditManager(address(cmMock)); @@ -1332,7 +1418,7 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve quotaChange: int96(int256(quotaInterestPerYear)) }); - psts.gaugeMock().updateEpoch(); + gaugeMock.updateEpoch(); vm.stopPrank(); } @@ -1345,7 +1431,7 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve uint256 expectedInterest = ((addLiquidity / 2) * borrowRate) / RAY; uint256 expectedLiquidity = addLiquidity + expectedInterest + (supportQuotas ? quotaInterestPerYear : 0); - uint256 expectedBorrowRate = psts.linearIRModel().calcBorrowRate(expectedLiquidity, addLiquidity / 2); + uint256 expectedBorrowRate = irm.calcBorrowRate(expectedLiquidity, addLiquidity / 2); _updateBorrowrate(); @@ -1381,8 +1467,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve } } - // [P4-17]: updateBorrowRate correctly updates parameters - function test_P4_17_changeQuotaRevenue_and_updateQuotaRevenue_updates_quotaRevenue_correctly() public { + // U:[P4-17]: updateBorrowRate correctly updates parameters + function test_U_P4_17_changeQuotaRevenue_and_updateQuotaRevenue_updates_quotaRevenue_correctly() public { _setUp(Tokens.DAI, true); address POOL_QUOTA_KEEPER = address(pqk); @@ -1428,8 +1514,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve assertEq(pool.expectedLiquidityLU(), (qu1 + qu2) / PERCENTAGE_FACTOR, "#3: Incorrect expectedLiquidityLU"); } - // [P4-18]: connectCreditManager, forbidCreditManagerToBorrow, newInterestRateModel, setExpecetedLiquidityLimit reverts if called with non-configurator - function test_P4_18_admin_functions_revert_on_non_admin() public { + // U:[P4-18]: connectCreditManager, forbidCreditManagerToBorrow, newInterestRateModel, setExpecetedLiquidityLimit reverts if called with non-configurator + function test_U_P4_18_admin_functions_revert_on_non_admin() public { vm.startPrank(USER); vm.expectRevert(CallerNotControllerException.selector); @@ -1439,7 +1525,7 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve pool.updateInterestRateModel(DUMB_ADDRESS); vm.expectRevert(CallerNotConfiguratorException.selector); - pool.connectPoolQuotaManager(DUMB_ADDRESS); + pool.setPoolQuotaManager(DUMB_ADDRESS); vm.expectRevert(CallerNotControllerException.selector); pool.setExpectedLiquidityLimit(0); @@ -1453,17 +1539,17 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve vm.stopPrank(); } - // [P4-19]: setCreditManagerLimit reverts if not in register - function test_P4_19_connectCreditManager_reverts_if_not_in_register() public { + // U:[P4-19]: setCreditManagerLimit reverts if not in register + function test_U_P4_19_connectCreditManager_reverts_if_not_in_register() public { vm.expectRevert(RegisteredCreditManagerOnlyException.selector); vm.prank(CONFIGURATOR); pool.setCreditManagerLimit(DUMB_ADDRESS, 1); } - // [P4-20]: setCreditManagerLimit reverts if another pool is setup in CreditManagerV3 - function test_P4_20_connectCreditManager_fails_on_incompatible_CM() public { - cmMock.changePoolService(DUMB_ADDRESS); + // U:[P4-20]: setCreditManagerLimit reverts if another pool is setup in CreditManagerV3 + function test_U_P4_20_connectCreditManager_fails_on_incompatible_CM() public { + cmMock.setPoolService(DUMB_ADDRESS); vm.expectRevert(IncompatibleCreditManagerException.selector); @@ -1471,8 +1557,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve pool.setCreditManagerLimit(address(cmMock), 1); } - // [P4-21]: setCreditManagerLimit connects manager first time, then update limit only - function test_P4_21_setCreditManagerLimit_connects_manager_first_time_then_update_limit_only() public { + // U:[P4-21]: setCreditManagerLimit connects manager first time, then update limit only + function test_U_P4_21_setCreditManagerLimit_connects_manager_first_time_then_update_limit_only() public { address[] memory cms = pool.creditManagers(); assertEq(cms.length, 0, "Credit manager is already connected!"); @@ -1508,8 +1594,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve assertEq(pool.creditManagerLimit(address(cmMock)), type(uint256).max, "#3: Incorrect CM limit"); } - // [P4-22]: updateInterestRateModel changes interest rate model & emit event - function test_P4_22_updateInterestRateModel_works_correctly_and_emits_event() public { + // U:[P4-22]: updateInterestRateModel changes interest rate model & emit event + function test_U_P4_22_updateInterestRateModel_works_correctly_and_emits_event() public { _setUpTestCase(Tokens.DAI, 0, 50_00, addLiquidity, 2 * RAY, 0, false); uint256 expectedLiquidity = pool.expectedLiquidity(); @@ -1543,11 +1629,11 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve ); } - // [P4-23]: connectPoolQuotaManager updates quotaRevenue and emits event + // U:[P4-23]: setPoolQuotaManager updates quotaRevenue and emits event - function test_P4_23_connectPoolQuotaManager_updates_quotaRevenue_and_emits_event() public { - pool = new Pool4626({ - _addressProvider: address(psts.addressProvider()), + function test_U_P4_23_setPoolQuotaManager_updates_quotaRevenue_and_emits_event() public { + pool = new PoolV3({ + _addressProvider: address(addressProvider), _underlyingToken: tokenTestSuite.addressOf(Tokens.DAI), _interestRateModel: address(irm), _expectedLiquidityLimit: type(uint256).max, @@ -1562,7 +1648,7 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve emit SetPoolQuotaKeeper(POOL_QUOTA_KEEPER); vm.prank(CONFIGURATOR); - pool.connectPoolQuotaManager(POOL_QUOTA_KEEPER); + pool.setPoolQuotaManager(POOL_QUOTA_KEEPER); uint96 qu = uint96(WAD * 10); @@ -1583,7 +1669,7 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve emit SetPoolQuotaKeeper(POOL_QUOTA_KEEPER2); vm.prank(CONFIGURATOR); - pool.connectPoolQuotaManager(POOL_QUOTA_KEEPER2); + pool.setPoolQuotaManager(POOL_QUOTA_KEEPER2); assertEq(pool.lastQuotaRevenueUpdate(), block.timestamp, "Incorrect lastQuotaRevenuUpdate"); assertEq(pool.quotaRevenue(), qu, "#1: Incorrect quotaRevenue"); @@ -1591,8 +1677,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve assertEq(pool.expectedLiquidityLU(), qu / PERCENTAGE_FACTOR, "Incorrect expectedLiquidityLU"); } - // [P4-24]: setExpectedLiquidityLimit() sets limit & emits event - function test_P4_24_setExpectedLiquidityLimit_correct_and_emits_event() public { + // U:[P4-24]: setExpectedLiquidityLimit() sets limit & emits event + function test_U_P4_24_setExpectedLiquidityLimit_correct_and_emits_event() public { vm.expectEmit(false, false, false, true); emit SetExpectedLiquidityLimit(10005); @@ -1602,8 +1688,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve assertEq(pool.expectedLiquidityLimit(), 10005, "expectedLiquidityLimit not set correctly"); } - // [P4-25]: setTotalBorrowedLimit sets limit & emits event - function test_P4_25_setTotalBorrowedLimit_correct_and_emits_event() public { + // U:[P4-25]: setTotalBorrowedLimit sets limit & emits event + function test_U_P4_25_setTotalBorrowedLimit_correct_and_emits_event() public { vm.expectEmit(false, false, false, true); emit SetTotalBorrowedLimit(10005); @@ -1613,8 +1699,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve assertEq(pool.totalBorrowedLimit(), 10005, "totalBorrowedLimit not set correctly"); } - // [P4-26]: setWithdrawFee works correctly - function test_P4_26_setWithdrawFee_works_correctly() public { + // U:[P4-26]: setWithdrawFee works correctly + function test_U_P4_26_setWithdrawFee_works_correctly() public { vm.expectRevert(IncorrectParameterException.selector); vm.prank(CONFIGURATOR); @@ -1643,8 +1729,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve uint256 expectedCanBorrow; } - // [P4-27]: creditManagerCanBorrow computes availabel borrow correctly - function test_P4_27_creditManagerCanBorrow_computes_available_borrow_amount_correctly() public { + // U:[P4-27]: creditManagerCanBorrow computes availabel borrow correctly + function test_U_P4_27_creditManagerCanBorrow_computes_available_borrow_amount_correctly() public { uint256 initialLiquidity = 10 * addLiquidity; CreditManagerBorrowTestCase[5] memory cases = [ CreditManagerBorrowTestCase({ @@ -1726,12 +1812,13 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve testCase.isBorrowingMoreU2Forbidden ); - CreditManagerMockForPoolTest cmMock2 = new CreditManagerMockForPoolTest( + CreditManagerMock cmMock2 = new CreditManagerMock( + address( addressProvider), address(pool) ); vm.startPrank(CONFIGURATOR); - psts.cr().addCreditManager(address(cmMock2)); + cr.addCreditManager(address(cmMock2)); pool.updateInterestRateModel(address(newIR)); pool.setTotalBorrowedLimit(type(uint256).max); @@ -1766,8 +1853,8 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve uint256 expectedSupplyRate; } - // [P4-28]: supplyRate computes rates correctly - function test_P4_28_supplyRate_computes_rates_correctly() public { + // U:[P4-28]: supplyRate computes rates correctly + function test_U_P4_28_supplyRate_computes_rates_correctly() public { SupplyRateTestCase[5] memory cases = [ SupplyRateTestCase({ name: "normal pool with zero debt and zero supply", @@ -1831,10 +1918,10 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve address POOL_QUOTA_KEEPER = address(pqk); vm.prank(CONFIGURATOR); - pool.connectPoolQuotaManager(POOL_QUOTA_KEEPER); + pool.setPoolQuotaManager(POOL_QUOTA_KEEPER); vm.prank(CONFIGURATOR); - pool.connectPoolQuotaManager(POOL_QUOTA_KEEPER); + pool.setPoolQuotaManager(POOL_QUOTA_KEEPER); vm.prank(POOL_QUOTA_KEEPER); pool.updateQuotaRevenue(testCase.quotaRevenue); @@ -1867,7 +1954,7 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve } // 10000000000000000 - // // [P4-23]: fromDiesel / toDiesel works correctly + // // U:[P4-23]: fromDiesel / toDiesel works correctly // function test_PX_23_diesel_conversion_is_correct() public { // _connectAndSetLimit(); @@ -1893,7 +1980,7 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve // ); // } - // // [P4-28]: expectedLiquidity() computes correctly + // // U:[P4-28]: expectedLiquidity() computes correctly // function test_PX_28_expectedLiquidity_correct() public { // _connectAndSetLimit(); @@ -1915,7 +2002,7 @@ contract Pool4626Test is TestHelper, BalanceHelper, IPool4626Events, IERC4626Eve // assertEq(pool.expectedLiquidity(), expectedLiquidity, "Index value was not updated correctly"); // } - // // [P4-35]: updateInterestRateModel reverts on zero address + // // U:[P4-35]: updateInterestRateModel reverts on zero address // function test_PX_35_updateInterestRateModel_reverts_on_zero_address() public { // vm.expectRevert(ZeroAddressException.selector); // vm.prank(CONFIGURATOR); diff --git a/contracts/test/unit/support/BotList.t.sol b/contracts/test/unit/support/BotList.t.sol index 3bd80942..93e39063 100644 --- a/contracts/test/unit/support/BotList.t.sol +++ b/contracts/test/unit/support/BotList.t.sol @@ -1,17 +1,21 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {BotList} from "../../../support/BotList.sol"; import {IBotListEvents, BotFunding} from "../../../interfaces/IBotList.sol"; +import {ICreditAccount} from "../../../interfaces/ICreditAccount.sol"; +import {ICreditManagerV3} from "../../../interfaces/ICreditManagerV3.sol"; +import {ICreditFacade} from "../../../interfaces/ICreditFacade.sol"; // TEST import "../../lib/constants.sol"; // MOCKS -import {AddressProviderACLMock} from "../../mocks/core/AddressProviderACLMock.sol"; +import {AddressProviderV3ACLMock} from "../../mocks/core/AddressProviderV3ACLMock.sol"; import {ERC20BlacklistableMock} from "../../mocks/token/ERC20Blacklistable.sol"; +import {GeneralMock} from "../../mocks/GeneralMock.sol"; // SUITES import {TokensTestSuite} from "../../suites/TokensTestSuite.sol"; @@ -23,22 +27,65 @@ import "../../../interfaces/IExceptions.sol"; import {Test} from "forge-std/Test.sol"; import "forge-std/console.sol"; +contract InvalidCFMock { + address public creditManager; + + constructor(address _creditManager) { + creditManager = _creditManager; + } +} + /// @title LPPriceFeedTest /// @notice Designed for unit test purposes only contract BotListTest is Test, IBotListEvents { - AddressProviderACLMock public addressProvider; + AddressProviderV3ACLMock public addressProvider; BotList botList; TokensTestSuite tokenTestSuite; + GeneralMock bot; + GeneralMock creditManager; + GeneralMock creditFacade; + GeneralMock creditAccount; + + address invalidCF; + function setUp() public { vm.prank(CONFIGURATOR); - addressProvider = new AddressProviderACLMock(); + addressProvider = new AddressProviderV3ACLMock(); tokenTestSuite = new TokensTestSuite(); botList = new BotList(address(addressProvider)); + + bot = new GeneralMock(); + creditManager = new GeneralMock(); + creditFacade = new GeneralMock(); + creditAccount = new GeneralMock(); + + 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(ICreditFacade.creditManager.selector), + abi.encode(address(creditManager)) + ); + + vm.mockCall( + address(creditAccount), + abi.encodeWithSelector(ICreditAccount.creditManager.selector), + abi.encode(address(creditManager)) + ); + + vm.prank(CONFIGURATOR); + botList.setApprovedCreditManagerStatus(address(creditManager), true); } /// @@ -49,7 +96,7 @@ contract BotListTest is Test, IBotListEvents { /// @dev [BL-1]: constructor sets correct values function test_BL_01_constructor_sets_correct_values() public { - assertEq(botList.treasury(), FRIEND2, "Treasury contract incorrect"); + assertEq(botList.treasury(), addressProvider.getTreasuryContract(), "Treasury contract incorrect"); assertEq(botList.daoFee(), 0, "Initial DAO fee incorrect"); } @@ -67,135 +114,359 @@ contract BotListTest is Test, IBotListEvents { assertEq(botList.daoFee(), 15, "DAO fee incorrect"); } - /// @dev [BL-3]: increaseBotFunding works correctly - function test_BL_03_increaseBotFunding_works_correctly() public { - vm.deal(USER, 10 ether); + /// @dev [BL-3]: setBotPermissions works correctly + function test_BL_03_setBotPermissions_works_correctly() public { + vm.expectRevert(CallerNotCreditFacadeException.selector); + vm.prank(invalidCF); + botList.setBotPermissions({ + creditAccount: address(creditAccount), + bot: address(bot), + permissions: type(uint192).max, + fundingAmount: uint72(1 ether), + weeklyFundingAllowance: uint72(1 ether / 10) + }); + + vm.expectRevert(abi.encodeWithSelector(AddressIsNotContractException.selector, DUMB_ADDRESS)); + vm.prank(address(creditFacade)); + botList.setBotPermissions({ + creditAccount: address(creditAccount), + bot: DUMB_ADDRESS, + permissions: type(uint192).max, + fundingAmount: uint72(1 ether), + weeklyFundingAllowance: uint72(1 ether / 10) + }); - vm.expectRevert(AmountCantBeZeroException.selector); - botList.increaseBotFunding(FRIEND); + vm.prank(CONFIGURATOR); + botList.setBotForbiddenStatus(address(bot), true); vm.expectRevert(InvalidBotException.selector); - vm.prank(USER); - botList.increaseBotFunding{value: 1 ether}(FRIEND); - - vm.prank(USER); - botList.setBotPermissions(address(addressProvider), type(uint192).max); + vm.prank(address(creditFacade)); + botList.setBotPermissions({ + creditAccount: address(creditAccount), + bot: address(bot), + permissions: type(uint192).max, + fundingAmount: uint72(1 ether), + weeklyFundingAllowance: uint72(1 ether / 10) + }); + + vm.expectRevert(PositiveFundingForInactiveBotException.selector); + vm.prank(address(creditFacade)); + botList.setBotPermissions({ + creditAccount: address(creditAccount), + bot: address(bot), + permissions: 0, + fundingAmount: uint72(1 ether), + weeklyFundingAllowance: uint72(1 ether / 10) + }); + + vm.prank(address(creditFacade)); + botList.setBotPermissions({ + creditAccount: address(creditAccount), + bot: address(bot), + permissions: 0, + fundingAmount: 0, + weeklyFundingAllowance: 0 + }); vm.prank(CONFIGURATOR); - botList.setBotForbiddenStatus(address(addressProvider), true); + botList.setBotForbiddenStatus(address(bot), false); - vm.expectRevert(InvalidBotException.selector); - vm.prank(USER); - botList.increaseBotFunding{value: 1 ether}(address(addressProvider)); + vm.expectEmit(true, true, false, true); + emit SetBotPermissions(address(creditAccount), address(bot), 1, uint72(1 ether), uint72(1 ether / 10)); - vm.prank(CONFIGURATOR); - botList.setBotForbiddenStatus(address(addressProvider), false); + vm.prank(address(creditFacade)); + uint256 activeBotsRemaining = botList.setBotPermissions({ + creditAccount: address(creditAccount), + bot: address(bot), + permissions: 1, + fundingAmount: uint72(1 ether), + weeklyFundingAllowance: uint72(1 ether / 10) + }); - vm.prank(USER); - botList.increaseBotFunding{value: 1 ether}(address(addressProvider)); + assertEq(activeBotsRemaining, 1, "Incorrect number of bots returned"); - (uint72 remainingFunds,,,) = botList.botFunding(USER, address(addressProvider)); + assertEq(botList.botPermissions(address(creditAccount), address(bot)), 1, "Bot permissions were not set"); - assertEq(remainingFunds, 1 ether, "Remaining funds incorrect"); + (uint256 remainingFunds, uint256 maxWeeklyAllowance, uint256 remainingWeeklyAllowance, uint256 allowanceLU) = + botList.botFunding(address(creditAccount), address(bot)); - vm.prank(USER); - botList.increaseBotFunding{value: 1 ether}(address(addressProvider)); + address[] memory bots = botList.getActiveBots(address(creditAccount)); - (remainingFunds,,,) = botList.botFunding(USER, address(addressProvider)); + assertEq(bots.length, 1, "Incorrect active bots array length"); - assertEq(remainingFunds, 2 ether, "Remaining funds incorrect"); - } + assertEq(bots[0], address(bot), "Incorrect address added to active bots list"); - /// @dev [BL-4]: decreaseBotFunding works correctly - function test_BL_04_decreaseBotFunding_works_correctly() public { - vm.deal(USER, 10 ether); + assertEq(remainingFunds, 1 ether, "Incorrect remaining funds value"); - vm.prank(USER); - botList.setBotPermissions(address(addressProvider), type(uint192).max); + assertEq(maxWeeklyAllowance, 1 ether / 10, "Incorrect max weekly allowance"); - vm.prank(USER); - botList.increaseBotFunding{value: 2 ether}(address(addressProvider)); + assertEq(remainingWeeklyAllowance, 1 ether / 10, "Incorrect remaining weekly allowance"); - vm.prank(USER); - botList.decreaseBotFunding(address(addressProvider), 1 ether); + assertEq(allowanceLU, block.timestamp, "Incorrect allowance update timestamp"); + + vm.prank(address(creditFacade)); + activeBotsRemaining = botList.setBotPermissions({ + creditAccount: address(creditAccount), + bot: address(bot), + permissions: 2, + fundingAmount: uint72(2 ether), + weeklyFundingAllowance: uint72(2 ether / 10) + }); + + (remainingFunds, maxWeeklyAllowance, remainingWeeklyAllowance, allowanceLU) = + botList.botFunding(address(creditAccount), address(bot)); + + assertEq(activeBotsRemaining, 1, "Incorrect number of bots returned"); + + assertEq(botList.botPermissions(address(creditAccount), address(bot)), 2, "Bot permissions were not set"); - (uint72 remainingFunds,,,) = botList.botFunding(USER, address(addressProvider)); + assertEq(remainingFunds, 2 ether, "Incorrect remaining funds value"); - assertEq(remainingFunds, 1 ether, "Remaining funds incorrect"); + assertEq(maxWeeklyAllowance, 2 ether / 10, "Incorrect max weekly allowance"); - assertEq(USER.balance, 9 ether, "USER was sent an incorrect amount"); + assertEq(remainingWeeklyAllowance, 2 ether / 10, "Incorrect remaining weekly allowance"); + + assertEq(allowanceLU, block.timestamp, "Incorrect allowance update timestamp"); + + bots = botList.getActiveBots(address(creditAccount)); + + assertEq(bots.length, 1, "Incorrect active bots array length"); + + assertEq(bots[0], address(bot), "Incorrect address added to active bots list"); + + vm.prank(address(creditFacade)); + activeBotsRemaining = botList.setBotPermissions({ + creditAccount: address(creditAccount), + bot: address(bot), + permissions: 0, + fundingAmount: 0, + weeklyFundingAllowance: 0 + }); + + (remainingFunds, maxWeeklyAllowance, remainingWeeklyAllowance, allowanceLU) = + botList.botFunding(address(creditAccount), address(bot)); + + assertEq(activeBotsRemaining, 0, "Incorrect number of bots returned"); + + assertEq(botList.botPermissions(address(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, block.timestamp, "Incorrect allowance update timestamp"); + + bots = botList.getActiveBots(address(creditAccount)); + + assertEq(bots.length, 0, "Incorrect active bots array length"); } - /// @dev [BL-5]: setWeeklyAllowance works correctly - function test_BL_05_setWeeklyAllowance_works_correctly() public { + /// @dev [BL-4]: addFunding and removeFunding work correctly + function test_BL_04_addFunding_removeFunding_work_correctly() public { vm.deal(USER, 10 ether); - vm.prank(USER); - botList.setBotPermissions(address(addressProvider), type(uint192).max); + vm.expectRevert(AmountCantBeZeroException.selector); + botList.addFunding(); + + vm.expectEmit(true, false, false, true); + emit ChangeFunding(USER, 1 ether); vm.prank(USER); - botList.setWeeklyBotAllowance(address(addressProvider), 1 ether); + botList.addFunding{value: 1 ether}(); - (, uint72 maxWeeklyAllowance,,) = botList.botFunding(USER, address(addressProvider)); + assertEq(botList.fundingBalances(USER), 1 ether, "User's bot funding wallet has incorrect balance"); - assertEq(maxWeeklyAllowance, 1 ether, "Incorrect new allowance"); + vm.expectEmit(true, false, false, true); + emit ChangeFunding(USER, 2 ether); vm.prank(USER); - botList.increaseBotFunding{value: 1 ether}(address(addressProvider)); + botList.addFunding{value: 1 ether}(); + + assertEq(botList.fundingBalances(USER), 2 ether, "User's bot funding wallet has incorrect balance"); - vm.prank(address(addressProvider)); - botList.pullPayment(USER, 1 ether / 10); + vm.expectEmit(true, false, false, true); + emit ChangeFunding(USER, 3 ether / 2); vm.prank(USER); - botList.setWeeklyBotAllowance(address(addressProvider), 1 ether / 2); + botList.removeFunding(1 ether / 2); - uint72 remainingWeeklyAllowance; + assertEq(botList.fundingBalances(USER), 3 ether / 2, "User's bot funding wallet has incorrect balance"); - (, maxWeeklyAllowance, remainingWeeklyAllowance,) = botList.botFunding(USER, address(addressProvider)); + assertEq(USER.balance, 85 ether / 10, "User's balance is incorrect"); + } - assertEq(maxWeeklyAllowance, 1 ether / 2, "Incorrect new allowance"); + /// @dev [BL-5]: payBot works correctly + function test_BL_05_payBot_works_correctly() public { + vm.prank(CONFIGURATOR); + botList.setDAOFee(5000); - assertEq(remainingWeeklyAllowance, 1 ether / 2, "Incorrect new remaining allowance"); - } + vm.mockCall( + address(creditManager), + abi.encodeWithSelector(ICreditManagerV3.getBorrowerOrRevert.selector, address(creditAccount)), + abi.encode(USER) + ); - /// @dev [BL-6]: pullPayment works correctly - function test_BL_06_pullPayment_works_correctly() public { vm.deal(USER, 10 ether); vm.prank(USER); - botList.setBotPermissions(address(addressProvider), type(uint192).max); + botList.addFunding{value: 2 ether}(); + + vm.prank(address(creditFacade)); + botList.setBotPermissions({ + creditAccount: address(creditAccount), + bot: address(bot), + permissions: 1, + fundingAmount: 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, + creditAccount: address(creditAccount), + bot: address(bot), + paymentAmount: uint72(1 ether / 20) + }); + + vm.expectEmit(true, true, true, true); + emit PayBot(USER, address(creditAccount), address(bot), uint72(1 ether / 20), uint72(1 ether / 40)); + + vm.prank(address(creditFacade)); + botList.payBot({ + payer: USER, + creditAccount: address(creditAccount), + bot: address(bot), + paymentAmount: uint72(1 ether / 20) + }); + + (uint256 remainingFunds, uint256 maxWeeklyAllowance, uint256 remainingWeeklyAllowance, uint256 allowanceLU) = + botList.botFunding(address(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.fundingBalances(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(address(bot).balance, 1 ether / 20, "Bot was sent incorrect ETH amount"); + + assertEq(addressProvider.getTreasuryContract().balance, 1 ether / 40, "Treasury was sent incorrect amount"); + + vm.warp(block.timestamp + 7 days); + + vm.prank(address(creditFacade)); + botList.payBot({ + payer: USER, + creditAccount: address(creditAccount), + bot: address(bot), + paymentAmount: uint72(1 ether / 20) + }); + + (remainingFunds, maxWeeklyAllowance, remainingWeeklyAllowance, allowanceLU) = + botList.botFunding(address(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.fundingBalances(USER), + 2 ether - (2 ether / 20) - (2 ether / 40), + "User remaining funding balance incorrect" + ); + + assertEq(address(bot).balance, 2 ether / 20, "Bot was sent incorrect ETH amount"); + + assertEq(addressProvider.getTreasuryContract().balance, 2 ether / 40, "Treasury was sent incorrect amount"); + } - vm.prank(USER); - botList.setWeeklyBotAllowance(address(addressProvider), 1 ether); + /// @dev [BL-6]: eraseAllBotPermissions works correctly + function test_BL_06_eraseAllBotPermissions_works_correctly() public { + vm.prank(address(creditFacade)); + botList.setBotPermissions({ + creditAccount: address(creditAccount), + bot: address(bot), + permissions: 1, + fundingAmount: uint72(1 ether), + weeklyFundingAllowance: uint72(1 ether / 10) + }); - vm.prank(USER); - botList.increaseBotFunding{value: 2 ether}(address(addressProvider)); + address bot2 = address(new GeneralMock()); - vm.prank(address(addressProvider)); - botList.pullPayment(USER, 1 ether / 10); + vm.prank(address(creditFacade)); + uint256 activeBotsRemaining = botList.setBotPermissions({ + creditAccount: address(creditAccount), + bot: address(bot2), + permissions: 2, + fundingAmount: uint72(2 ether), + weeklyFundingAllowance: uint72(2 ether / 10) + }); - (uint72 remainingFunds,, uint72 remainingWeeklyAllowance,) = botList.botFunding(USER, address(addressProvider)); + assertEq(activeBotsRemaining, 2, "Incorrect number of active bots"); - assertEq(remainingFunds, 2 ether - 1 ether / 10, "Incorrect new remaining funds"); + vm.expectRevert(CallerNotCreditFacadeException.selector); + vm.prank(invalidCF); + botList.eraseAllBotPermissions(address(creditAccount)); - assertEq(remainingWeeklyAllowance, 1 ether - 1 ether / 10, "Incorrect new remaining allowance"); + vm.expectEmit(true, false, false, false); + emit EraseBots(address(creditAccount)); - assertEq(address(addressProvider).balance, 1 ether / 10, "Incorrect amount sent to bot"); + vm.prank(address(creditFacade)); + botList.eraseAllBotPermissions(address(creditAccount)); - vm.prank(CONFIGURATOR); - botList.setDAOFee(10000); + assertEq( + botList.botPermissions(address(creditAccount), address(bot)), 0, "Permissions were not erased for bot 1" + ); + + assertEq( + botList.botPermissions(address(creditAccount), address(bot2)), 0, "Permissions were not erased for bot 2" + ); + + (uint256 remainingFunds, uint256 maxWeeklyAllowance, uint256 remainingWeeklyAllowance, uint256 allowanceLU) = + botList.botFunding(address(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"); + + assertEq(allowanceLU, block.timestamp, "Allowance update timestamp incorrect"); + + (remainingFunds, maxWeeklyAllowance, remainingWeeklyAllowance, allowanceLU) = + botList.botFunding(address(creditAccount), address(bot2)); - vm.prank(address(addressProvider)); - botList.pullPayment(USER, 1 ether / 10); + assertEq(remainingFunds, 0, "Remaining funds were not zeroed"); - (remainingFunds,, remainingWeeklyAllowance,) = botList.botFunding(USER, address(addressProvider)); + assertEq(maxWeeklyAllowance, 0, "Remaining funds were not zeroed"); - assertEq(remainingFunds, 2 ether - 3 ether / 10, "Incorrect new remaining funds"); + assertEq(remainingWeeklyAllowance, 0, "Remaining funds were not zeroed"); - assertEq(remainingWeeklyAllowance, 1 ether - 3 ether / 10, "Incorrect new remaining allowance"); + assertEq(allowanceLU, block.timestamp, "Allowance update timestamp incorrect"); - assertEq(address(addressProvider).balance, 2 ether / 10, "Incorrect amount sent to bot"); + address[] memory activeBots = botList.getActiveBots(address(creditAccount)); - assertEq(FRIEND2.balance, 1 ether / 10, "Incorrect amount sent to treasury"); + assertEq(activeBots.length, 0, "Not all active bots were disabled"); } } diff --git a/contracts/test/unit/support/ControllerTimelock.t.sol b/contracts/test/unit/support/ControllerTimelock.t.sol index a1105cbe..656c0424 100644 --- a/contracts/test/unit/support/ControllerTimelock.t.sol +++ b/contracts/test/unit/support/ControllerTimelock.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {ControllerTimelock} from "../../../support/risk-controller/ControllerTimelock.sol"; import {Policy} from "../../../support/risk-controller/PolicyManager.sol"; @@ -11,8 +11,8 @@ import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/P import {ICreditManagerV3} from "../../../interfaces/ICreditManagerV3.sol"; import {ICreditFacade} from "../../../interfaces/ICreditFacade.sol"; import {ICreditConfigurator} from "../../../interfaces/ICreditConfiguratorV3.sol"; -import {IPool4626} from "../../../interfaces/IPool4626.sol"; -import {Pool4626} from "../../../pool/Pool4626.sol"; +import {IPoolV3} from "../../../interfaces/IPoolV3.sol"; +import {PoolV3} from "../../../pool/PoolV3.sol"; import {ILPPriceFeed} from "../../../interfaces/ILPPriceFeed.sol"; import {IControllerTimelockEvents, IControllerTimelockErrors} from "../../../interfaces/IControllerTimelock.sol"; @@ -21,13 +21,13 @@ import "../../lib/constants.sol"; import {Test} from "forge-std/Test.sol"; // MOCKS -import {AddressProviderACLMock} from "../../mocks/core/AddressProviderACLMock.sol"; +import {AddressProviderV3ACLMock} from "../../mocks/core/AddressProviderV3ACLMock.sol"; // EXCEPTIONS import "../../../interfaces/IExceptions.sol"; contract ControllerTimelockTest is Test, IControllerTimelockEvents, IControllerTimelockErrors { - AddressProviderACLMock public addressProvider; + AddressProviderV3ACLMock public addressProvider; ControllerTimelock public controllerTimelock; @@ -39,7 +39,7 @@ contract ControllerTimelockTest is Test, IControllerTimelockEvents, IControllerT vetoAdmin = makeAddr("VETO_ADMIN"); vm.prank(CONFIGURATOR); - addressProvider = new AddressProviderACLMock(); + addressProvider = new AddressProviderV3ACLMock(); controllerTimelock = new ControllerTimelock(address(addressProvider), admin, vetoAdmin); } @@ -90,7 +90,7 @@ contract ControllerTimelockTest is Test, IControllerTimelockEvents, IControllerT ); vm.mockCall( - pool, abi.encodeWithSelector(IPool4626.creditManagerBorrowed.selector, creditManager), abi.encode(1234) + pool, abi.encodeWithSelector(IPoolV3.creditManagerBorrowed.selector, creditManager), abi.encode(1234) ); Policy memory policy = Policy({ @@ -126,9 +126,7 @@ contract ControllerTimelockTest is Test, IControllerTimelockEvents, IControllerT vm.prank(admin); controllerTimelock.setExpirationDate(creditManager, uint40(block.timestamp + 5)); - vm.mockCall( - pool, abi.encodeWithSelector(IPool4626.creditManagerBorrowed.selector, creditManager), abi.encode(0) - ); + vm.mockCall(pool, abi.encodeWithSelector(IPoolV3.creditManagerBorrowed.selector, creditManager), abi.encode(0)); // VERIFY THAT THE FUNCTION IS QUEUED AND EXECUTED CORRECTLY bytes32 txHash = keccak256( @@ -395,9 +393,7 @@ contract ControllerTimelockTest is Test, IControllerTimelockEvents, IControllerT bytes32 POLICY_CODE = keccak256(abi.encode("CM", "CREDIT_MANAGER_DEBT_LIMIT")); - vm.mockCall( - pool, abi.encodeWithSelector(IPool4626.creditManagerLimit.selector, creditManager), abi.encode(1e18) - ); + vm.mockCall(pool, abi.encodeWithSelector(IPoolV3.creditManagerLimit.selector, creditManager), abi.encode(1e18)); Policy memory policy = Policy({ enabled: false, @@ -449,7 +445,7 @@ contract ControllerTimelockTest is Test, IControllerTimelockEvents, IControllerT vm.prank(admin); controllerTimelock.setCreditManagerDebtLimit(creditManager, 2e18); - vm.expectCall(pool, abi.encodeWithSelector(Pool4626.setCreditManagerLimit.selector, creditManager, 2e18)); + vm.expectCall(pool, abi.encodeWithSelector(PoolV3.setCreditManagerLimit.selector, creditManager, 2e18)); vm.warp(block.timestamp + 1 days); @@ -497,24 +493,36 @@ contract ControllerTimelockTest is Test, IControllerTimelockEvents, IControllerT // VERIFY THAT THE FUNCTION IS ONLY CALLABLE BY ADMIN vm.expectRevert(CallerNotAdminException.selector); vm.prank(USER); - controllerTimelock.rampLiquidationThreshold(creditManager, token, 6000, 7 days); + controllerTimelock.rampLiquidationThreshold( + creditManager, token, 6000, uint40(block.timestamp + 14 days), 7 days + ); // VERIFY THAT POLICY CHECKS ARE PERFORMED vm.expectRevert(ParameterChecksFailedException.selector); vm.prank(admin); - controllerTimelock.rampLiquidationThreshold(creditManager, token, 5000, 7 days); + controllerTimelock.rampLiquidationThreshold( + creditManager, token, 5000, uint40(block.timestamp + 14 days), 7 days + ); // VERIFY THAT EXTRA CHECKS ARE PERFORMED vm.expectRevert(ParameterChecksFailedException.selector); vm.prank(admin); - controllerTimelock.rampLiquidationThreshold(creditManager, token, 6000, 1 days); + controllerTimelock.rampLiquidationThreshold( + creditManager, token, 6000, uint40(block.timestamp + 14 days), 1 days + ); + + vm.expectRevert(ParameterChecksFailedException.selector); + vm.prank(admin); + controllerTimelock.rampLiquidationThreshold( + creditManager, token, 6000, uint40(block.timestamp + 1 days / 2), 7 days + ); // VERIFY THAT THE FUNCTION IS QUEUED AND EXECUTED CORRECTLY bytes32 txHash = keccak256( abi.encode( creditConfigurator, - "rampLiquidationThreshold(address,uint16,uint24)", - abi.encode(token, 6000, 7 days), + "rampLiquidationThreshold(address,uint16,uint40,uint24)", + abi.encode(token, 6000, block.timestamp + 14 days, 7 days), block.timestamp + 1 days ) ); @@ -523,17 +531,25 @@ contract ControllerTimelockTest is Test, IControllerTimelockEvents, IControllerT emit QueueTransaction( txHash, creditConfigurator, - "rampLiquidationThreshold(address,uint16,uint24)", - abi.encode(token, 6000, 7 days), + "rampLiquidationThreshold(address,uint16,uint40,uint24)", + abi.encode(token, 6000, block.timestamp + 14 days, 7 days), uint40(block.timestamp + 1 days) ); vm.prank(admin); - controllerTimelock.rampLiquidationThreshold(creditManager, token, 6000, 7 days); + controllerTimelock.rampLiquidationThreshold( + creditManager, token, 6000, uint40(block.timestamp + 14 days), 7 days + ); vm.expectCall( creditConfigurator, - abi.encodeWithSelector(ICreditConfigurator.rampLiquidationThreshold.selector, token, 6000, 7 days) + abi.encodeWithSelector( + ICreditConfigurator.rampLiquidationThreshold.selector, + token, + 6000, + uint40(block.timestamp + 14 days), + 7 days + ) ); vm.warp(block.timestamp + 1 days); @@ -556,9 +572,7 @@ contract ControllerTimelockTest is Test, IControllerTimelockEvents, IControllerT creditFacade, abi.encodeWithSelector(ICreditFacade.expirationDate.selector), abi.encode(block.timestamp) ); - vm.mockCall( - pool, abi.encodeWithSelector(IPool4626.creditManagerBorrowed.selector, creditManager), abi.encode(0) - ); + vm.mockCall(pool, abi.encodeWithSelector(IPoolV3.creditManagerBorrowed.selector, creditManager), abi.encode(0)); Policy memory policy = Policy({ enabled: false, @@ -660,14 +674,14 @@ contract ControllerTimelockTest is Test, IControllerTimelockEvents, IControllerT creditFacade, abi.encodeWithSelector(ICreditFacade.expirationDate.selector), abi.encode(block.timestamp) ); - vm.mockCall( - pool, abi.encodeWithSelector(IPool4626.creditManagerBorrowed.selector, creditManager), abi.encode(0) - ); + vm.mockCall(pool, abi.encodeWithSelector(IPoolV3.creditManagerBorrowed.selector, creditManager), abi.encode(0)); + + uint40 expirationDate = uint40(block.timestamp + 2 days); Policy memory policy = Policy({ enabled: false, flags: 1, - exactValue: block.timestamp + 5, + exactValue: expirationDate, minValue: 0, maxValue: 0, referencePoint: 0, @@ -682,15 +696,15 @@ contract ControllerTimelockTest is Test, IControllerTimelockEvents, IControllerT vm.prank(CONFIGURATOR); controllerTimelock.setPolicy(POLICY_CODE, policy); - uint40 expirationDate = uint40(block.timestamp + 1 days); - // VERIFY THAT THE FUNCTION IS QUEUED AND EXECUTED CORRECTLY bytes32 txHash = keccak256( - abi.encode(creditConfigurator, "setExpirationDate(uint40)", abi.encode(block.timestamp + 5), expirationDate) + abi.encode( + creditConfigurator, "setExpirationDate(uint40)", abi.encode(expirationDate), block.timestamp + 1 days + ) ); vm.prank(admin); - controllerTimelock.setExpirationDate(creditManager, uint40(block.timestamp + 5)); + controllerTimelock.setExpirationDate(creditManager, expirationDate); vm.expectRevert(CallerNotAdminException.selector); @@ -709,20 +723,17 @@ contract ControllerTimelockTest is Test, IControllerTimelockEvents, IControllerT vm.warp(block.timestamp - 10 days); - // vm.mockCallRevert( - // creditConfigurator, - // abi.encodeWithSelector( - // ICreditConfigurator.setExpirationDate.selector, - // expirationDate - // ), - // abi.encode("error") - // ); + vm.mockCallRevert( + creditConfigurator, + abi.encodeWithSelector(ICreditConfigurator.setExpirationDate.selector, expirationDate), + abi.encode("error") + ); - // vm.expectRevert(TxExecutionRevertedException.selector); - // vm.prank(admin); - // controllerTimelock.executeTransaction(txHash); + vm.expectRevert(TxExecutionRevertedException.selector); + vm.prank(admin); + controllerTimelock.executeTransaction(txHash); - // vm.clearMockedCalls(); + vm.clearMockedCalls(); vm.expectEmit(true, false, false, false); emit ExecuteTransaction(txHash); diff --git a/contracts/test/unit/support/GearStaking.t.sol b/contracts/test/unit/support/GearStaking.t.sol index eceb8884..39d5f954 100644 --- a/contracts/test/unit/support/GearStaking.t.sol +++ b/contracts/test/unit/support/GearStaking.t.sol @@ -1,19 +1,19 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {GearStaking} from "../../../support/GearStaking.sol"; import {IGearStakingEvents, MultiVote, VotingContractStatus} from "../../../interfaces/IGearStaking.sol"; import {IVotingContract} from "../../../interfaces/IVotingContract.sol"; - import {CallerNotConfiguratorException} from "../../../interfaces/IExceptions.sol"; +import "../../../interfaces/IAddressProviderV3.sol"; // TEST import "../../lib/constants.sol"; // MOCKS -import {AddressProviderACLMock} from "../../mocks/core/AddressProviderACLMock.sol"; +import {AddressProviderV3ACLMock} from "../../mocks/core/AddressProviderV3ACLMock.sol"; import {ERC20Mock} from "@gearbox-protocol/core-v2/contracts/test/mocks/token/ERC20Mock.sol"; import {TargetContractMock} from "@gearbox-protocol/core-v2/contracts/test/mocks/adapters/TargetContractMock.sol"; @@ -32,7 +32,7 @@ uint256 constant EPOCH_LENGTH = 7 days; contract GearStakingTest is Test, IGearStakingEvents { address gearToken; - AddressProviderACLMock public addressProvider; + AddressProviderV3ACLMock public addressProvider; GearStaking gearStaking; @@ -42,13 +42,14 @@ contract GearStakingTest is Test, IGearStakingEvents { function setUp() public { vm.prank(CONFIGURATOR); - addressProvider = new AddressProviderACLMock(); + addressProvider = new AddressProviderV3ACLMock(); tokenTestSuite = new TokensTestSuite(); gearToken = tokenTestSuite.addressOf(Tokens.WETH); - addressProvider.setGearToken(gearToken); + vm.prank(CONFIGURATOR); + addressProvider.setAddress(AP_GEAR_TOKEN, gearToken, false); gearStaking = new GearStaking(address(addressProvider), block.timestamp + 1); diff --git a/contracts/test/unit/support/PolicyManager.t.sol b/contracts/test/unit/support/PolicyManager.t.sol index 86244b90..187301c9 100644 --- a/contracts/test/unit/support/PolicyManager.t.sol +++ b/contracts/test/unit/support/PolicyManager.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {PolicyManager, Policy} from "../../../support/risk-controller/PolicyManager.sol"; import {PolicyManagerInternal} from "../../mocks/support/PolicyManagerInternal.sol"; @@ -11,20 +11,20 @@ import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/P import "../../lib/constants.sol"; // MOCKS -import {AddressProviderACLMock} from "../../mocks/core/AddressProviderACLMock.sol"; +import {AddressProviderV3ACLMock} from "../../mocks/core/AddressProviderV3ACLMock.sol"; import {Test} from "forge-std/Test.sol"; // EXCEPTIONS import "../../../interfaces/IExceptions.sol"; contract PolicyManagerTest is Test { - AddressProviderACLMock public addressProvider; + AddressProviderV3ACLMock public addressProvider; PolicyManagerInternal public policyManager; function setUp() public { vm.prank(CONFIGURATOR); - addressProvider = new AddressProviderACLMock(); + addressProvider = new AddressProviderV3ACLMock(); policyManager = new PolicyManagerInternal(address(addressProvider)); } diff --git a/contracts/test/unit/support/WithdrawalManager.t.sol b/contracts/test/unit/support/WithdrawalManager.t.sol index c20b6bce..8ca76a51 100644 --- a/contracts/test/unit/support/WithdrawalManager.t.sol +++ b/contracts/test/unit/support/WithdrawalManager.t.sol @@ -9,40 +9,18 @@ import {ClaimAction, IWithdrawalManagerEvents, ScheduledWithdrawal} from "../../ import { AmountCantBeZeroException, CallerNotConfiguratorException, - CallerNotCreditManagerException, NoFreeWithdrawalSlotsException, NothingToClaimException, + RegisteredCreditManagerOnlyException, ZeroAddressException } from "../../../interfaces/IExceptions.sol"; -import {WithdrawalManager} from "../../../support/WithdrawalManager.sol"; import {USER} from "../../lib/constants.sol"; import {TestHelper} from "../../lib/helper.sol"; -import {AddressProviderACLMock} from "../../mocks/core/AddressProviderACLMock.sol"; +import {AddressProviderV3ACLMock} from "../../mocks/core/AddressProviderV3ACLMock.sol"; import {ERC20BlacklistableMock} from "../../mocks/token/ERC20Blacklistable.sol"; -contract WithdrawalManagerHarness is WithdrawalManager { - constructor(address _addressProvider, uint40 _delay) WithdrawalManager(_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); - } -} +import {WithdrawalManagerHarness} from "./WithdrawalManagerHarness.sol"; enum ScheduleTask { IMMATURE, @@ -50,11 +28,11 @@ enum ScheduleTask { NON_SCHEDULED } -/// @title Withdrawal manager test -/// @notice [WM]: Unit tests for withdrawal manager -contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { +/// @title Withdrawal manager unit test +/// @notice U:[WM]: Unit tests for withdrawal manager +contract WithdrawalManagerUnitTest is TestHelper, IWithdrawalManagerEvents { WithdrawalManagerHarness manager; - AddressProviderACLMock acl; + AddressProviderV3ACLMock acl; ERC20BlacklistableMock token0; ERC20Mock token1; @@ -76,9 +54,9 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { creditManager = makeAddr("CREDIT_MANAGER"); vm.startPrank(configurator); - acl = new AddressProviderACLMock(); + acl = new AddressProviderV3ACLMock(); + acl.addCreditManager(creditManager); manager = new WithdrawalManagerHarness(address(acl), DELAY); - manager.setCreditManagerStatus(creditManager, true); vm.stopPrank(); token0 = new ERC20BlacklistableMock("Test token 1", "TEST1", 18); @@ -89,30 +67,27 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { /// GENERAL TESTS /// /// ------------- /// - /// @notice [WM-1]: Constructor sets correct values - function test_WM_01_constructor_sets_correct_values() public { + /// @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 [WM-2]: External functions have correct access - function test_WM_02_external_functions_have_correct_access() public { + /// @notice U:[WM-2]: External functions have correct access + function test_U_WM_02_external_functions_have_correct_access() public { vm.startPrank(USER); - vm.expectRevert(CallerNotCreditManagerException.selector); + vm.expectRevert(RegisteredCreditManagerOnlyException.selector); manager.addImmediateWithdrawal(address(0), address(0), 0); - vm.expectRevert(CallerNotCreditManagerException.selector); + vm.expectRevert(RegisteredCreditManagerOnlyException.selector); manager.addScheduledWithdrawal(address(0), address(0), 0, 0); - vm.expectRevert(CallerNotCreditManagerException.selector); + vm.expectRevert(RegisteredCreditManagerOnlyException.selector); manager.claimScheduledWithdrawals(address(0), address(0), ClaimAction(0)); vm.expectRevert(CallerNotConfiguratorException.selector); manager.setWithdrawalDelay(0); - vm.expectRevert(CallerNotConfiguratorException.selector); - manager.setCreditManagerStatus(address(0), false); - vm.stopPrank(); } @@ -120,8 +95,8 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { /// IMMEDIATE WITHDRAWALS TESTS /// /// --------------------------- /// - /// @notice [WM-3]: `addImmediateWithdrawal` works correctly - function test_WM_03_addImmediateWithdrawal_works_correctly() public { + /// @notice U:[WM-3]: `addImmediateWithdrawal` works correctly + function test_U_WM_03_addImmediateWithdrawal_works_correctly() public { vm.startPrank(creditManager); // add first withdrawal @@ -130,7 +105,7 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { vm.expectEmit(true, true, false, true); emit AddImmediateWithdrawal(USER, address(token0), AMOUNT); - manager.addImmediateWithdrawal(USER, address(token0), AMOUNT); + manager.addImmediateWithdrawal(address(token0), USER, AMOUNT); assertEq( manager.immediateWithdrawals(USER, address(token0)), @@ -144,7 +119,7 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { vm.expectEmit(true, true, false, true); emit AddImmediateWithdrawal(USER, address(token0), AMOUNT); - manager.addImmediateWithdrawal(USER, address(token0), AMOUNT); + manager.addImmediateWithdrawal(address(token0), USER, AMOUNT); assertEq( manager.immediateWithdrawals(USER, address(token0)), @@ -155,26 +130,26 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { vm.stopPrank(); } - /// @notice [WM-4A]: `claimImmediateWithdrawal` reverts on zero recipient - function test_WM_04A_claimImmediateWithdrawal_reverts_on_zero_recipient() public { + /// @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 [WM-4B]: `claimImmediateWithdrawal` reverts on nothing to claim - function test_WM_04B_claimImmediateWithdrawal_reverts_on_nothing_to_claim() public { + /// @notice U:[WM-4B]: `claimImmediateWithdrawal` reverts on nothing to claim + function test_U_WM_04B_claimImmediateWithdrawal_reverts_on_nothing_to_claim() public { vm.expectRevert(NothingToClaimException.selector); vm.prank(USER); manager.claimImmediateWithdrawal(address(token0), address(USER)); } - /// @notice [WM-4C]: `claimImmediateWithdrawal` works correctly - function test_WM_04C_claimImmediateWithdrawal_works_correctly() public { + /// @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(USER, address(token0), 10 ether); + manager.addImmediateWithdrawal(address(token0), USER, 10 ether); vm.expectEmit(true, true, false, true); emit ClaimImmediateWithdrawal(USER, address(token0), USER, AMOUNT - 1); @@ -190,8 +165,8 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { /// SCHEDULED WITHDRAWALS: EXTERNAL FUNCTIONS TESTS /// /// ----------------------------------------------- /// - /// @notice [WM-5A]: `addScheduledWithdrawal` reverts on zero amount - function test_WM_05A_addScheduledWithdrawal_reverts_on_zero_amount() public { + /// @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); @@ -207,8 +182,8 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { uint8 expectedSlot; } - /// @notice [WM-5B]: `addScheduledWithdrawal` works correctly - function test_WM_05B_addScheduledWithdrawal_works_correctly() public { + /// @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", @@ -269,8 +244,8 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { } } - /// @notice [WM-6A]: `claimScheduledWithdrawals` reverts on nothing to claim when action is `CLAIM` - function test_WM_06A_claimScheduledWithdrawals_reverts_on_nothing_to_claim() public { + /// @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); @@ -293,8 +268,8 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { uint256 expectedTokensToEnable; } - /// @notice [WM-6B]: `claimScheduledWithdrawals` works correctly - function test_WM_06B_claimScheduledWithdrawals_works_correctly() public { + /// @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", @@ -406,8 +381,8 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { uint256 expectedAmount1; } - /// @notice [WM-7]: `cancellableScheduledWithdrawals` works correctly - function test_WM_07_cancellableScheduledWithdrawals_works_correctly() public { + /// @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", @@ -485,8 +460,8 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { uint256 expectedTokensToEnable; } - /// @notice [WM-8]: `_processScheduledWithdrawal` works correctly - function test_WM_08_processScheduledWithdrawal_works_correctly() public { + /// @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", @@ -641,8 +616,8 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { } } - /// @notice [WM-9A]: `_claimScheduledWithdrawal` works correctly - function test_WM_09A_claimScheduledWithdrawal_works_correctly() public { + /// @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); @@ -657,8 +632,8 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { assertEq(w.maturity, 1, "Withdrawal not cleared"); } - /// @notice [WM-9B]: `_claimScheduledWithdrawal` works correctly with blacklisted recipient - function test_WM_09B_claimScheduledWithdrawal_works_correctly_with_blacklisted_recipient() public { + /// @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); @@ -676,8 +651,8 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { assertEq(w.maturity, 1, "Withdrawal not cleared"); } - /// @notice [WM-10]: `_cancelScheduledWithdrawal` works correctly - function test_WM_10_cancelScheduledWithdrawal_works_correctly() public { + /// @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); @@ -694,8 +669,8 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { /// CONFIGURATION TESTS /// /// ------------------- /// - /// @notice [WM-12]: `setWithdrawalDelay` works correctly - function test_WM_12_setWithdrawalDelay_works_correctly() public { + /// @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); @@ -707,19 +682,6 @@ contract WithdrawalManagerTest is TestHelper, IWithdrawalManagerEvents { assertEq(manager.delay(), newDelay, "Incorrect delay"); } - /// @notice [WM-11]: `setCreditManagerStatus` works correctly - function test_WM_11_setCreditManagerStatus_works_correctly() public { - address newCreditManager = makeAddr("NEW_CREDIT_MANAGER"); - - vm.expectEmit(true, false, false, true); - emit SetCreditManagerStatus(newCreditManager, true); - - vm.prank(configurator); - manager.setCreditManagerStatus(newCreditManager, true); - - assertTrue(manager.creditManagerStatus(newCreditManager), "Incorrect credit manager status"); - } - /// ------- /// /// HELPERS /// /// ------- /// diff --git a/contracts/test/unit/support/WithdrawalManagerHarness.sol b/contracts/test/unit/support/WithdrawalManagerHarness.sol new file mode 100644 index 00000000..828c904e --- /dev/null +++ b/contracts/test/unit/support/WithdrawalManagerHarness.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2023 +pragma solidity ^0.8.17; + +import {ClaimAction, ScheduledWithdrawal} from "../../../interfaces/IWithdrawalManager.sol"; +import {WithdrawalManager} from "../../../support/WithdrawalManager.sol"; + +contract WithdrawalManagerHarness is WithdrawalManager { + constructor(address _addressProvider, uint40 _delay) WithdrawalManager(_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/traits/USDT_TransferUnitTest.t.sol b/contracts/test/unit/traits/USDT_TransferUnitTest.t.sol new file mode 100644 index 00000000..24df5c5b --- /dev/null +++ b/contracts/test/unit/traits/USDT_TransferUnitTest.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +import {USDT_Transfer} from "../../../traits/USDT_Transfer.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {TestHelper} from "../../lib/helper.sol"; +import "forge-std/console.sol"; + +contract USDT_TransferUnitTest is USDT_Transfer, TestHelper { + using Math for uint256; + + uint256 public basisPointsRate; + + uint256 public maximumFee; + + constructor() USDT_Transfer(address(this)) {} + + /// @dev U:[UTT_01]: amountUSDTWithFee computes value correctly [fuzzing] + function test_U_UTT_01_fuzzing_amountUSDTWithFee_computes_value_correctly(uint256 amount, uint8 fee, uint256 maxFee) + public + { + uint256 decimals = 6; + uint256 tenBillionsUSDT = 10 ** 10 * (10 ** decimals); + uint256 oneCent = (10 ** decimals) / 100; + vm.assume(amount < tenBillionsUSDT); + vm.assume(fee < 100_00); // fee could not be more than 100% + vm.assume(maxFee < 100 * (10 ** decimals)); // we assume that transfer will cost could not exceed $100 + + basisPointsRate = fee; + maximumFee = maxFee; + + uint256 value = _amountUSDTMinusFee(_amountUSDTWithFee(amount)); + uint256 diff = Math.max(amount, value) - Math.min(amount, value); + assertTrue(diff < oneCent, "Incorrect computation"); + } +} diff --git a/contracts/traits/ACLNonReentrantTrait.sol b/contracts/traits/ACLNonReentrantTrait.sol index 34d754f4..3f4a490f 100644 --- a/contracts/traits/ACLNonReentrantTrait.sol +++ b/contracts/traits/ACLNonReentrantTrait.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol"; import {AddressProvider} from "@gearbox-protocol/core-v2/contracts/core/AddressProvider.sol"; @@ -15,21 +15,17 @@ import { } from "../interfaces/IExceptions.sol"; import {ACLTrait} from "./ACLTrait.sol"; +import {ReentrancyGuardTrait} from "./ReentrancyGuardTrait.sol"; /// @title ACL Trait /// @notice Utility class for ACL consumers -abstract contract ACLNonReentrantTrait is ACLTrait, Pausable { +abstract contract ACLNonReentrantTrait is ACLTrait, ReentrancyGuardTrait, Pausable { /// @dev Emitted when new external controller is set event NewController(address indexed newController); - uint8 private constant _NOT_ENTERED = 1; - uint8 private constant _ENTERED = 2; - address public controller; bool public externalController; - uint8 private _status = _NOT_ENTERED; - /// @dev Ensures that caller is external controller (if it is set) or configurator modifier controllerOnly() { if (externalController) { @@ -60,26 +56,6 @@ abstract contract ACLNonReentrantTrait is ACLTrait, Pausable { _; } - /// @dev Prevents a contract from calling itself, directly or indirectly. - /// Calling a `nonReentrant` function from another `nonReentrant` - /// function is not supported. It is possible to prevent this from happening - /// by making the `nonReentrant` function external, and making it call a - /// `private` function that does the actual work. - /// - modifier nonReentrant() { - // On the first call to nonReentrant, _notEntered will be true - require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); - - // Any calls to nonReentrant after this point will fail - _status = _ENTERED; - - _; - - // By storing the original value once again, a refund is triggered (see - // https://eips.ethereum.org/EIPS/eip-2200) - _status = _NOT_ENTERED; - } - /// @dev constructor /// @param addressProvider Address of address repository constructor(address addressProvider) ACLTrait(addressProvider) nonZeroAddress(addressProvider) { diff --git a/contracts/traits/ACLTrait.sol b/contracts/traits/ACLTrait.sol index d09f57aa..288f2901 100644 --- a/contracts/traits/ACLTrait.sol +++ b/contracts/traits/ACLTrait.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; import {AddressProvider} from "@gearbox-protocol/core-v2/contracts/core/AddressProvider.sol"; import {IACL} from "@gearbox-protocol/core-v2/contracts/interfaces/IACL.sol"; diff --git a/contracts/traits/ContractsRegisterTrait.sol b/contracts/traits/ContractsRegisterTrait.sol index 68396d98..1c9049c6 100644 --- a/contracts/traits/ContractsRegisterTrait.sol +++ b/contracts/traits/ContractsRegisterTrait.sol @@ -3,7 +3,7 @@ // (c) Gearbox Holdings, 2022 pragma solidity ^0.8.17; -import {AddressProvider} from "@gearbox-protocol/core-v2/contracts/core/AddressProvider.sol"; +import "../interfaces/IAddressProviderV3.sol"; import {ContractsRegister} from "@gearbox-protocol/core-v2/contracts/core/ContractsRegister.sol"; import { @@ -20,29 +20,35 @@ abstract contract ContractsRegisterTrait is SanityCheckTrait { // ACL contract to check rights ContractsRegister immutable _cr; + /// @dev Checks that credit manager is registered + modifier registeredCreditManagerOnly(address addr) { + _checkRegisteredCreditManagerOnly(addr); + _; + } + + /// @dev Checks that credit manager is registered + modifier registeredPoolOnly(address addr) { + _checkRegisteredPoolOnly(addr); + _; + } + constructor(address addressProvider) nonZeroAddress(addressProvider) { - _cr = ContractsRegister(AddressProvider(addressProvider).getContractsRegister()); + _cr = ContractsRegister(IAddressProviderV3(addressProvider).getAddressOrRevert(AP_CONTRACTS_REGISTER, 1)); } function isRegisteredPool(address _pool) internal view returns (bool) { return _cr.isPool(_pool); } - function isRegisteredCreditManager(address _pool) internal view returns (bool) { - return _cr.isCreditManager(_pool); + function isRegisteredCreditManager(address _creditManager) internal view returns (bool) { + return _cr.isCreditManager(_creditManager); } - /// @dev Checks that credit manager is registered - modifier registeredCreditManagerOnly(address addr) { - if (!isRegisteredCreditManager(addr)) revert RegisteredCreditManagerOnlyException(); // T:[WG-3] - - _; + function _checkRegisteredCreditManagerOnly(address addr) internal view { + if (!isRegisteredCreditManager(addr)) revert RegisteredCreditManagerOnlyException(); } - /// @dev Checks that credit manager is registered - modifier registeredPoolOnly(address addr) { + function _checkRegisteredPoolOnly(address addr) internal view { if (!isRegisteredPool(addr)) revert RegisteredPoolOnlyException(); - - _; } } diff --git a/contracts/traits/ReentrancyGuardTrait.sol b/contracts/traits/ReentrancyGuardTrait.sol new file mode 100644 index 00000000..d163dd55 --- /dev/null +++ b/contracts/traits/ReentrancyGuardTrait.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Holdings, 2022 +pragma solidity ^0.8.17; + +uint8 constant NOT_ENTERED = 1; +uint8 constant ENTERED = 2; + +/// @title ReentrancyGuardTrait +abstract contract ReentrancyGuardTrait { + uint8 internal _reentrancyStatus = NOT_ENTERED; + + /// @dev Prevents a contract from calling itself, directly or indirectly. + /// Calling a `nonReentrant` function from another `nonReentrant` + /// function is not supported. It is possible to prevent this from happening + /// by making the `nonReentrant` function external, and making it call a + /// `private` function that does the actual work. + /// + modifier nonReentrant() { + // On the first call to nonReentrant, _notEntered will be true + require(_reentrancyStatus != ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _reentrancyStatus = ENTERED; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _reentrancyStatus = NOT_ENTERED; + } +} diff --git a/contracts/traits/SanityCheckTrait.sol b/contracts/traits/SanityCheckTrait.sol index 8f55f1b6..f73a8a29 100644 --- a/contracts/traits/SanityCheckTrait.sol +++ b/contracts/traits/SanityCheckTrait.sol @@ -9,7 +9,11 @@ import {ZeroAddressException} from "../interfaces/IExceptions.sol"; /// @notice Utility class for ACL consumers abstract contract SanityCheckTrait { modifier nonZeroAddress(address addr) { - if (addr == address(0)) revert ZeroAddressException(); // F:[P4-2] + _nonZeroCheck(addr); _; } + + function _nonZeroCheck(address addr) private pure { + if (addr == address(0)) revert ZeroAddressException(); // F:[P4-2] + } } diff --git a/contracts/traits/USDT_Transfer.sol b/contracts/traits/USDT_Transfer.sol index 7fe7d404..b8397412 100644 --- a/contracts/traits/USDT_Transfer.sol +++ b/contracts/traits/USDT_Transfer.sol @@ -1,15 +1,19 @@ // SPDX-License-Identifier: MIT // Gearbox Protocol. Generalized leverage for DeFi protocols // (c) Gearbox Holdings, 2022 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IUSDT} from "../interfaces/external/IUSDT.sol"; +import {USDTFees} from "../libraries/USDTFees.sol"; import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol"; +interface IUSDT { + function basisPointsRate() external view returns (uint256); + + function maximumFee() external view returns (uint256); +} + contract USDT_Transfer { - using SafeERC20 for IERC20; + using USDTFees for uint256; address private immutable usdt; @@ -17,32 +21,17 @@ contract USDT_Transfer { usdt = _usdt; } - function _safeUSDTTransfer(address to, uint256 amount) internal returns (uint256) { - uint256 balanceBefore = IERC20(usdt).balanceOf(to); - IERC20(usdt).balanceOf(msg.sender); - - IERC20(usdt).safeTransferFrom(msg.sender, to, amount); - - return IERC20(usdt).balanceOf(to) - balanceBefore; - } - /// @dev Computes how much usdt you should send to get exact amount on destination account function _amountUSDTWithFee(uint256 amount) internal view virtual returns (uint256) { - uint256 amountWithBP = (amount * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR - IUSDT(usdt).basisPointsRate()); - uint256 maximumFee = IUSDT(usdt).maximumFee(); - unchecked { - uint256 amountWithMaxFee = maximumFee > type(uint256).max - amount ? maximumFee : amount + maximumFee; - return amountWithBP > amountWithMaxFee ? amountWithMaxFee : amountWithBP; - } + uint256 basisPointsRate = IUSDT(usdt).basisPointsRate(); // U:[UTT_01] + uint256 maximumFee = IUSDT(usdt).maximumFee(); // U:[UTT_01] + return amount.amountUSDTWithFee({basisPointsRate: basisPointsRate, maximumFee: maximumFee}); // U:[UTT_01] } /// @dev Computes how much usdt you should send to get exact amount on destination account function _amountUSDTMinusFee(uint256 amount) internal view virtual returns (uint256) { - uint256 fee = amount * IUSDT(usdt).basisPointsRate() / 10000; - uint256 maximumFee = IUSDT(usdt).maximumFee(); - if (fee > maximumFee) { - fee = maximumFee; - } - return amount - fee; + uint256 basisPointsRate = IUSDT(usdt).basisPointsRate(); // U:[UTT_01] + uint256 maximumFee = IUSDT(usdt).maximumFee(); // U:[UTT_01] + return amount.amountUSDTMinusFee({basisPointsRate: basisPointsRate, maximumFee: maximumFee}); // U:[UTT_01] } } diff --git a/foundry.toml b/foundry.toml index 56fa85f6..d09ae7bf 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ libs = ['lib'] out = 'forge-out' solc_version = '0.8.17' src = 'contracts' -optimizer_runs = 20000 +optimizer_runs = 8000 # See more config options https://github.com/gakonst/foundry/tree/master/config block_number = 120000 @@ -11,3 +11,6 @@ block_timestamp = 16400000 gas_limit = 9223372036854775807 # the gas limit in tests block_base_fee_per_gas = 100 fs_permissions = [{ access = "read-write", path = "./"}] + +[fuzz] +max_test_rejects = 200000 diff --git a/lib/forge-std b/lib/forge-std index 73a504d2..73d44ec7 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 73a504d2cf6f37b7ce285b479f4c681f76e95f1b +Subproject commit 73d44ec7d124e3831bc5f832267889ffb6f9bc3f