diff --git a/contracts/credit/CreditFacadeV3.sol b/contracts/credit/CreditFacadeV3.sol index e2af75d4..489ac892 100644 --- a/contracts/credit/CreditFacadeV3.sol +++ b/contracts/credit/CreditFacadeV3.sol @@ -10,6 +10,7 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {BalancesLogic, Balance, BalanceWithMask} from "../libraries/BalancesLogic.sol"; import {ACLNonReentrantTrait} from "../traits/ACLNonReentrantTrait.sol"; import {BitMask, UNDERLYING_TOKEN_MASK} from "../libraries/BitMask.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; // DATA import {MultiCall} from "@gearbox-protocol/core-v2/contracts/libraries/MultiCall.sol"; @@ -59,6 +60,12 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { using BitMask for uint256; using SafeCast for uint256; + /// @notice Contract version + uint256 public constant override version = 3_00; + + /// @notice maxDebt to maxQuota multiplier + uint256 public constant maxQuotaMultiplier = 8; + /// @notice Credit Manager connected to this Credit Facade address public immutable creditManager; @@ -116,9 +123,6 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { /// And are compensated by the Gearbox DAO separately. mapping(address => bool) public override canLiquidateWhilePaused; - /// @notice Contract version - uint256 public constant override version = 3_00; - /// @notice Restricts functions to the connected Credit Configurator only modifier creditConfiguratorOnly() { _checkCreditConfigurator(); @@ -948,8 +952,13 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { { int96 realQuotaChange; (address token, int96 quotaChange, uint96 minQuota) = abi.decode(callData, (address, int96, uint96)); // U:[FA-34] - (realQuotaChange, tokensToEnable, tokensToDisable) = - ICreditManagerV3(creditManager).updateQuota(creditAccount, token, quotaChange, minQuota); // U:[FA-34] + (realQuotaChange, tokensToEnable, tokensToDisable) = ICreditManagerV3(creditManager).updateQuota({ + creditAccount: creditAccount, + token: token, + quotaChange: quotaChange, + minQuota: minQuota, + maxQuota: uint96(Math.min(type(uint96).max, maxQuotaMultiplier * debtLimits.maxDebt)) + }); // U:[FA-34] emit UpdateQuota({creditAccount: creditAccount, token: token, quotaChange: realQuotaChange}); // U:[FA-34] } diff --git a/contracts/credit/CreditManagerV3.sol b/contracts/credit/CreditManagerV3.sol index 417177d1..92fcca75 100644 --- a/contracts/credit/CreditManagerV3.sol +++ b/contracts/credit/CreditManagerV3.sol @@ -951,7 +951,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT /// @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, uint96 minQuota) + function updateQuota(address creditAccount, address token, int96 quotaChange, uint96 minQuota, uint96 maxQuota) external override nonReentrant // U:[CM-5] @@ -970,7 +970,8 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT creditAccount: creditAccount, token: token, quotaChange: quotaChange, - minQuota: minQuota + minQuota: minQuota, + maxQuota: maxQuota }); // U:[CM-25] // I: [CMQ-3] if (enable) { diff --git a/contracts/interfaces/ICreditManagerV3.sol b/contracts/interfaces/ICreditManagerV3.sol index d447fb04..11620d26 100644 --- a/contracts/interfaces/ICreditManagerV3.sol +++ b/contracts/interfaces/ICreditManagerV3.sol @@ -187,7 +187,7 @@ interface ICreditManagerV3 is ICreditManagerV3Events, IVersion { /// @dev Updates credit account's quotas /// @param creditAccount Address of credit account - function updateQuota(address creditAccount, address token, int96 quotaChange, uint96 minQuota) + function updateQuota(address creditAccount, address token, int96 quotaChange, uint96 minQuota, uint96 maxQuota) external returns (int96 realQuotaChange, uint256 tokensToEnable, uint256 tokensToDisable); diff --git a/contracts/interfaces/IExceptions.sol b/contracts/interfaces/IExceptions.sol index c84cc4c3..37b5962f 100644 --- a/contracts/interfaces/IExceptions.sol +++ b/contracts/interfaces/IExceptions.sol @@ -213,4 +213,4 @@ error InsufficientBalanceException(); error OpenCloseAccountInOneBlockException(); -error QuotaLessThanMinialException(); +error QuotaIsOutOfBoundsException(); diff --git a/contracts/interfaces/IPoolQuotaKeeperV3.sol b/contracts/interfaces/IPoolQuotaKeeperV3.sol index 3475a306..2b237cf2 100644 --- a/contracts/interfaces/IPoolQuotaKeeperV3.sol +++ b/contracts/interfaces/IPoolQuotaKeeperV3.sol @@ -47,7 +47,7 @@ interface IPoolQuotaKeeperV3 is IPoolQuotaKeeperV3Events, IVersion { /// @param creditAccount Address of credit account /// @param token Address of the token to change the quota for /// @param quotaChange Requested quota change in pool's underlying asset units - function updateQuota(address creditAccount, address token, int96 quotaChange, uint96 minQuota) + function updateQuota(address creditAccount, address token, int96 quotaChange, uint96 minQuota, uint96 maxQuota) external returns (uint256 caQuotaInterestChange, int96 change, bool enableToken, bool disableToken); diff --git a/contracts/libraries/QuotasLogic.sol b/contracts/libraries/QuotasLogic.sol index 3f66df5a..c9184e15 100644 --- a/contracts/libraries/QuotasLogic.sol +++ b/contracts/libraries/QuotasLogic.sol @@ -11,6 +11,7 @@ import {CreditLogic} from "./CreditLogic.sol"; import {RAY, SECONDS_PER_YEAR, PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; import "../interfaces/IExceptions.sol"; +import "forge-std/console.sol"; uint192 constant RAY_DIVIDED_BY_PERCENTAGE = uint192(RAY / PERCENTAGE_FACTOR); diff --git a/contracts/pool/PoolQuotaKeeperV3.sol b/contracts/pool/PoolQuotaKeeperV3.sol index 6fae36c5..6dcb9b54 100644 --- a/contracts/pool/PoolQuotaKeeperV3.sol +++ b/contracts/pool/PoolQuotaKeeperV3.sol @@ -84,8 +84,8 @@ contract PoolQuotaKeeperV3 is IPoolQuotaKeeperV3, ACLNonReentrantTrait, Contract ACLNonReentrantTrait(IPoolV3(_pool).addressProvider()) ContractsRegisterTrait(IPoolV3(_pool).addressProvider()) { - pool = _pool; // F:[PQK-1] - underlying = IPoolV3(_pool).asset(); // F:[PQK-1] + pool = _pool; // U:[PQK-1] + underlying = IPoolV3(_pool).asset(); // U:[PQK-1] } /// @notice Updates a Credit Account's quota amount for a token @@ -100,10 +100,10 @@ contract PoolQuotaKeeperV3 is IPoolQuotaKeeperV3, ACLNonReentrantTrait, Contract /// at capacity. /// @return enableToken Whether the token needs to be enabled /// @return disableToken Whether the token needs to be disabled - function updateQuota(address creditAccount, address token, int96 quotaChange, uint96 minQuota) + function updateQuota(address creditAccount, address token, int96 quotaChange, uint96 minQuota, uint96 maxQuota) external override - creditManagerOnly // F:[PQK-4] + creditManagerOnly // U:[PQK-4] returns (uint256 caQuotaInterestChange, int96 realQuotaChange, bool enableToken, bool disableToken) { int256 quotaRevenueChange; @@ -130,9 +130,9 @@ contract PoolQuotaKeeperV3 is IPoolQuotaKeeperV3, ACLNonReentrantTrait, Contract accountQuota: accountQuota, lastQuotaRateUpdate: lastQuotaRateUpdate, quotaChange: quotaChange - }); + }); // U:[PQK-14] - if (accountQuota.quota < minQuota) revert QuotaLessThanMinialException(); + if (accountQuota.quota < minQuota || accountQuota.quota > maxQuota) revert QuotaIsOutOfBoundsException(); /// Quota revenue must be changed on each quota updated, so that the /// pool can correctly compute its liquidity metrics in the future @@ -148,7 +148,7 @@ contract PoolQuotaKeeperV3 is IPoolQuotaKeeperV3, ACLNonReentrantTrait, Contract function removeQuotas(address creditAccount, address[] memory tokens, bool setLimitsToZero) external override - creditManagerOnly // F:[PQK-4] + creditManagerOnly // U:[PQK-4] { int256 quotaRevenueChange; @@ -195,7 +195,7 @@ contract PoolQuotaKeeperV3 is IPoolQuotaKeeperV3, ACLNonReentrantTrait, Contract function accrueQuotaInterest(address creditAccount, address[] memory tokens) external override - creditManagerOnly // F:[PQK-4] + creditManagerOnly // U:[PQK-4] { uint256 len = tokens.length; uint40 _lastQuotaRateUpdate = lastQuotaRateUpdate; @@ -281,28 +281,28 @@ contract PoolQuotaKeeperV3 is IPoolQuotaKeeperV3, ACLNonReentrantTrait, Contract /// @param token Address of the token function addQuotaToken(address token) external - gaugeOnly // F:[PQK-3] + gaugeOnly // U:[PQK-3] { if (quotaTokensSet.contains(token)) { - revert TokenAlreadyAddedException(); // F:[PQK-6] + revert TokenAlreadyAddedException(); // U:[PQK-6] } /// The interest rate is not set immediately on adding a quoted token, /// since all rates are updated during a general epoch update in the Gauge - quotaTokensSet.add(token); // F:[PQK-5] - totalQuotaParams[token].initialise(); // F:[PQK-5] + quotaTokensSet.add(token); // U:[PQK-5] + totalQuotaParams[token].initialise(); // U:[PQK-5] - emit NewQuotaTokenAdded(token); // F:[PQK-5] + emit NewQuotaTokenAdded(token); // U:[PQK-5] } /// @notice Batch updates the quota rates and changes the combined quota revenue function updateRates() external override - gaugeOnly // F:[PQK-3] + gaugeOnly // U:[PQK-3] { address[] memory tokens = quotaTokensSet.values(); - uint16[] memory rates = IGaugeV3(gauge).getRates(tokens); // F:[PQK-7] + uint16[] memory rates = IGaugeV3(gauge).getRates(tokens); // U:[PQK-7] uint256 quotaRevenue; uint256 timestampLU = lastQuotaRateUpdate; @@ -314,18 +314,18 @@ contract PoolQuotaKeeperV3 is IPoolQuotaKeeperV3, ACLNonReentrantTrait, Contract /// Before writing a new rate, the token's interest index current value is also /// saved, to ensure that further calculations with the new rates are correct - TokenQuotaParams storage tokenQuotaParams = totalQuotaParams[token]; - quotaRevenue += tokenQuotaParams.updateRate(timestampLU, rate); + TokenQuotaParams storage tokenQuotaParams = totalQuotaParams[token]; // U:[PQK-7] + quotaRevenue += tokenQuotaParams.updateRate(timestampLU, rate); // U:[PQK-7] - emit UpdateTokenQuotaRate(token, rate); // F:[PQK-7] + emit UpdateTokenQuotaRate(token, rate); // U:[PQK-7] unchecked { ++i; } } - IPoolV3(pool).setQuotaRevenue(quotaRevenue); // F:[PQK-7] - lastQuotaRateUpdate = uint40(block.timestamp); // F:[PQK-7] + IPoolV3(pool).setQuotaRevenue(quotaRevenue); // U:[PQK-7] + lastQuotaRateUpdate = uint40(block.timestamp); // U:[PQK-7] } // @@ -336,12 +336,12 @@ contract PoolQuotaKeeperV3 is IPoolQuotaKeeperV3, ACLNonReentrantTrait, Contract /// @param _gauge The new contract's address function setGauge(address _gauge) external - configuratorOnly // F:[PQK-2] + configuratorOnly // U:[PQK-2] { if (gauge != _gauge) { - gauge = _gauge; // F:[PQK-8] - lastQuotaRateUpdate = uint40(block.timestamp); // F:[PQK-8] - emit SetGauge(_gauge); // F:[PQK-8] + gauge = _gauge; // U:[PQK-8] + lastQuotaRateUpdate = uint40(block.timestamp); // U:[PQK-8] + emit SetGauge(_gauge); // U:[PQK-8] } } @@ -349,18 +349,18 @@ contract PoolQuotaKeeperV3 is IPoolQuotaKeeperV3, ACLNonReentrantTrait, Contract /// @param _creditManager Address of the new Credit Manager function addCreditManager(address _creditManager) external - configuratorOnly // F:[PQK-2] + configuratorOnly // U:[PQK-2] nonZeroAddress(_creditManager) - registeredCreditManagerOnly(_creditManager) // F:[PQK-9] + registeredCreditManagerOnly(_creditManager) // U:[PQK-9] { - if (ICreditManagerV3(_creditManager).pool() != address(pool)) { - revert IncompatibleCreditManagerException(); // F:[PQK-9] + if (ICreditManagerV3(_creditManager).pool() != pool) { + revert IncompatibleCreditManagerException(); // U:[PQK-9] } /// Checks if creditManager is already in list if (!creditManagerSet.contains(_creditManager)) { - creditManagerSet.add(_creditManager); // F:[PQK-10] - emit AddCreditManager(_creditManager); // F:[PQK-10] + creditManagerSet.add(_creditManager); // U:[PQK-10] + emit AddCreditManager(_creditManager); // U:[PQK-10] } } @@ -369,7 +369,7 @@ contract PoolQuotaKeeperV3 is IPoolQuotaKeeperV3, ACLNonReentrantTrait, Contract /// @param limit The limit to set function setTokenLimit(address token, uint96 limit) external - controllerOnly // F:[PQK-2] + controllerOnly // U:[PQK-2] { TokenQuotaParams storage tokenQuotaParams = totalQuotaParams[token]; _setTokenLimit(tokenQuotaParams, token, limit); @@ -377,18 +377,23 @@ contract PoolQuotaKeeperV3 is IPoolQuotaKeeperV3, ACLNonReentrantTrait, Contract /// @dev IMPLEMENTATION: setTokenLimit function _setTokenLimit(TokenQuotaParams storage tokenQuotaParams, address token, uint96 limit) internal { + // setLimit checks that token is initialize, otherwise it reverts + // F:[PQK-11] if (tokenQuotaParams.setLimit(limit)) { - emit SetTokenLimit(token, limit); - } // F:[PQK-12] + emit SetTokenLimit(token, limit); // U:[PQK-12] + } } /// @notice Sets the one-time fee paid on each quota increase /// @param token Token to set the fee for /// @param fee The new fee value in PERCENTAGE_FACTOR format - function setTokenQuotaIncreaseFee(address token, uint16 fee) external controllerOnly { - TokenQuotaParams storage tokenQuotaParams = totalQuotaParams[token]; + function setTokenQuotaIncreaseFee(address token, uint16 fee) + external + controllerOnly // U:[PQK-2] + { + TokenQuotaParams storage tokenQuotaParams = totalQuotaParams[token]; // U:[PQK-13] if (tokenQuotaParams.setQuotaIncreaseFee(fee)) { - emit SetQuotaIncreaseFee(token, fee); + emit SetQuotaIncreaseFee(token, fee); // U:[PQK-13] } } } diff --git a/contracts/test/integration/credit/CreditManager_Quotas.int.t.sol b/contracts/test/integration/credit/CreditManager_Quotas.int.t.sol index 60627620..451409fd 100644 --- a/contracts/test/integration/credit/CreditManager_Quotas.int.t.sol +++ b/contracts/test/integration/credit/CreditManager_Quotas.int.t.sol @@ -163,13 +163,20 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper address link = tokenTestSuite.addressOf(Tokens.LINK); vm.expectRevert(CallerNotCreditFacadeException.selector); vm.prank(FRIEND); - creditManager.updateQuota({creditAccount: creditAccount, token: link, quotaChange: 100_000, minQuota: 0}); + creditManager.updateQuota({ + creditAccount: creditAccount, + token: link, + quotaChange: 100_000, + minQuota: 0, + maxQuota: type(uint96).max + }); } vm.expectCall( address(poolQuotaKeeper), abi.encodeCall( - IPoolQuotaKeeperV3.updateQuota, (creditAccount, tokenTestSuite.addressOf(Tokens.LINK), 100_000, 0) + IPoolQuotaKeeperV3.updateQuota, + (creditAccount, tokenTestSuite.addressOf(Tokens.LINK), 100_000, 0, type(uint96).max) ) ); @@ -179,7 +186,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: 100_000, - minQuota: 0 + minQuota: 0, + maxQuota: type(uint96).max }); assertEq(tokensToEnable, linkMask, "Incorrect tokensToEnble"); @@ -191,7 +199,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: -100_000, - minQuota: 0 + minQuota: 0, + maxQuota: type(uint96).max }); assertEq(tokensToEnable, 0, "Incorrect tokensToEnable"); assertEq(tokensToDisable, linkMask, "Incorrect tokensToDisable"); @@ -207,7 +216,13 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper { address usdc = tokenTestSuite.addressOf(Tokens.USDC); vm.expectRevert(TokenIsNotQuotedException.selector); - creditManager.updateQuota({creditAccount: creditAccount, token: usdc, quotaChange: 100_000, minQuota: 0}); + creditManager.updateQuota({ + creditAccount: creditAccount, + token: usdc, + quotaChange: 100_000, + minQuota: 0, + maxQuota: type(uint96).max + }); } } @@ -225,7 +240,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: 100_000, - minQuota: 0 + minQuota: 0, + maxQuota: type(uint96).max }); enabledTokensMask |= tokensToEnable; @@ -234,7 +250,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.USDT), quotaChange: 200_000, - minQuota: 0 + minQuota: 0, + maxQuota: type(uint96).max }); enabledTokensMask |= tokensToEnable; @@ -280,7 +297,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: 100_000, - minQuota: 0 + minQuota: 0, + maxQuota: type(uint96).max }); enabledTokensMask |= tokensToEnable; @@ -289,7 +307,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.USDT), quotaChange: 200_000, - minQuota: 0 + minQuota: 0, + maxQuota: type(uint96).max }); enabledTokensMask |= tokensToEnable; @@ -332,7 +351,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: int96(uint96(100 * WAD)), - minQuota: 0 + minQuota: 0, + maxQuota: type(uint96).max }); enabledTokensMask |= tokensToEnable; @@ -340,7 +360,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.USDT), quotaChange: int96(uint96(200 * WAD)), - minQuota: 0 + minQuota: 0, + maxQuota: type(uint96).max }); enabledTokensMask |= tokensToEnable; @@ -410,7 +431,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: int96(quotaLink), - minQuota: 0 + minQuota: 0, + maxQuota: type(uint96).max }); enabledTokensMask |= tokensToEnable; @@ -418,7 +440,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.USDT), quotaChange: int96(quotaUsdt), - minQuota: 0 + minQuota: 0, + maxQuota: type(uint96).max }); enabledTokensMask |= tokensToEnable; @@ -464,7 +487,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: int96(uint96(100 * WAD)), - minQuota: 0 + minQuota: 0, + maxQuota: type(uint96).max }); enabledTokensMap |= tokensToEnable; @@ -472,7 +496,8 @@ contract CreditManagerQuotasTest is Test, ICreditManagerV3Events, BalanceHelper creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.USDT), quotaChange: int96(uint96(200 * WAD)), - minQuota: 0 + minQuota: 0, + maxQuota: type(uint96).max }); enabledTokensMap |= tokensToEnable; diff --git a/contracts/test/mocks/credit/CreditManagerMock.sol b/contracts/test/mocks/credit/CreditManagerMock.sol index 8e762303..6776a0c3 100644 --- a/contracts/test/mocks/credit/CreditManagerMock.sol +++ b/contracts/test/mocks/credit/CreditManagerMock.sol @@ -130,7 +130,7 @@ contract CreditManagerMock { qu_tokensToDisable = tokensToDisable; } - function updateQuota(address, address, int96, uint96) + function updateQuota(address, address, int96, uint96, uint96) external view returns (int96 change, uint256 tokensToEnable, uint256 tokensToDisable) diff --git a/contracts/test/mocks/pool/GaugeMock.sol b/contracts/test/mocks/pool/GaugeMock.sol index a07560aa..bd398305 100644 --- a/contracts/test/mocks/pool/GaugeMock.sol +++ b/contracts/test/mocks/pool/GaugeMock.sol @@ -60,14 +60,14 @@ contract GaugeMock is ACLNonReentrantTrait { keeper.updateRates(); } - function addQuotaToken(address token, uint16 _rate) external configuratorOnly { - rates[token] = _rate; + function addQuotaToken(address token, uint16 rate) external configuratorOnly { + rates[token] = rate; IPoolQuotaKeeperV3 keeper = IPoolQuotaKeeperV3(pool.poolQuotaKeeper()); keeper.addQuotaToken(token); } - function changeQuotaTokenRateParams(address token, uint16 _rate) external configuratorOnly { - rates[token] = _rate; + function changeQuotaTokenRateParams(address token, uint16 rate) external configuratorOnly { + rates[token] = rate; } function getRates(address[] memory tokens) external view returns (uint16[] memory result) { diff --git a/contracts/test/mocks/pool/PoolQuotaKeeperMock.sol b/contracts/test/mocks/pool/PoolQuotaKeeperMock.sol index f0a9aa61..856a846b 100644 --- a/contracts/test/mocks/pool/PoolQuotaKeeperMock.sol +++ b/contracts/test/mocks/pool/PoolQuotaKeeperMock.sol @@ -51,7 +51,7 @@ contract PoolQuotaKeeperMock is IPoolQuotaKeeperV3 { underlying = _underlying; } - function updateQuota(address, address, int96, uint96) + function updateQuota(address, address, int96, uint96, uint96) external view returns (uint256 caQuotaInterestChange, int96 realQuotaChange, bool enableToken, bool disableToken) diff --git a/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol b/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol index 21519bf6..883df6d8 100644 --- a/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol +++ b/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol @@ -1772,6 +1772,11 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve function test_U_FA_34_multicall_updateQuota_works_properly() public notExpirableCase { address creditAccount = DUMB_ADDRESS; + uint96 maxDebt = 443330; + + vm.prank(CONFIGURATOR); + creditFacade.setDebtLimits(0, maxDebt, type(uint8).max); + address link = tokenTestSuite.addressOf(Tokens.LINK); uint256 maskToEnable = 1 << 4; uint256 maskToDisable = 1 << 7; @@ -1781,7 +1786,11 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve creditManagerMock.setUpdateQuota({change: change, tokensToEnable: maskToEnable, tokensToDisable: maskToDisable}); vm.expectCall( - address(creditManagerMock), abi.encodeCall(ICreditManagerV3.updateQuota, (creditAccount, link, change, 0)) + address(creditManagerMock), + abi.encodeCall( + ICreditManagerV3.updateQuota, + (creditAccount, link, change, 0, uint96(maxDebt * creditFacade.maxQuotaMultiplier())) + ) ); vm.expectEmit(true, true, false, false); diff --git a/contracts/test/unit/credit/CreditManagerV3.unit.t.sol b/contracts/test/unit/credit/CreditManagerV3.unit.t.sol index 0ed436b9..d4b6e324 100644 --- a/contracts/test/unit/credit/CreditManagerV3.unit.t.sol +++ b/contracts/test/unit/credit/CreditManagerV3.unit.t.sol @@ -406,7 +406,7 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH creditManager.fullCollateralCheck(DUMB_ADDRESS, 0, new uint256[](0), 1); vm.expectRevert(CallerNotCreditFacadeException.selector); - creditManager.updateQuota(DUMB_ADDRESS, DUMB_ADDRESS, 0, 0); + creditManager.updateQuota(DUMB_ADDRESS, DUMB_ADDRESS, 0, 0, 0); vm.expectRevert(CallerNotCreditFacadeException.selector); creditManager.scheduleWithdrawal(DUMB_ADDRESS, DUMB_ADDRESS, 0); @@ -504,7 +504,7 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH creditManager.fullCollateralCheck(DUMB_ADDRESS, 0, new uint256[](0), 1); vm.expectRevert("ReentrancyGuard: reentrant call"); - creditManager.updateQuota(DUMB_ADDRESS, DUMB_ADDRESS, 0, 0); + creditManager.updateQuota(DUMB_ADDRESS, DUMB_ADDRESS, 0, 0, 0); vm.expectRevert("ReentrancyGuard: reentrant call"); creditManager.scheduleWithdrawal(DUMB_ADDRESS, DUMB_ADDRESS, 0); @@ -2185,7 +2185,8 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH creditAccount: creditAccount, token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: 122, - minQuota: 122 + minQuota: 122, + maxQuota: type(uint96).max }); (,, uint256 cumulativeQuotaInterest,,,,) = creditManager.creditAccountInfo(creditAccount); diff --git a/contracts/test/unit/pool/PoolQuotaKeeper.unit.t.sol b/contracts/test/unit/pool/PoolQuotaKeeper.unit.t.sol index 822d7fc3..1ed85afc 100644 --- a/contracts/test/unit/pool/PoolQuotaKeeper.unit.t.sol +++ b/contracts/test/unit/pool/PoolQuotaKeeper.unit.t.sol @@ -44,10 +44,10 @@ contract PoolQuotaKeeperUnitTest is TestHelper, BalanceHelper, IPoolQuotaKeeperV PoolQuotaKeeperV3 pqk; GaugeMock gaugeMock; - PoolMock pool; + PoolMock poolMock; address underlying; - CreditManagerMock cmMock; + CreditManagerMock creditManagerMock; function setUp() public { _setUp(Tokens.DAI); @@ -63,26 +63,26 @@ contract PoolQuotaKeeperUnitTest is TestHelper, BalanceHelper, IPoolQuotaKeeperV AddressProviderV3ACLMock addressProvider = new AddressProviderV3ACLMock(); addressProvider.setAddress(AP_WETH_TOKEN, tokenTestSuite.addressOf(Tokens.WETH), false); - pool = new PoolMock(address(addressProvider), underlying); + poolMock = new PoolMock(address(addressProvider), underlying); - pqk = new PoolQuotaKeeperV3(address(pool)); + pqk = new PoolQuotaKeeperV3(address(poolMock)); - pool.setPoolQuotaKeeper(address(pqk)); + poolMock.setPoolQuotaKeeper(address(pqk)); - gaugeMock = new GaugeMock(address(pool)); + gaugeMock = new GaugeMock(address(poolMock)); pqk.setGauge(address(gaugeMock)); vm.startPrank(CONFIGURATOR); - cmMock = new CreditManagerMock(address(addressProvider), address(pool)); + creditManagerMock = new CreditManagerMock(address(addressProvider), address(poolMock)); cr = ContractsRegister(addressProvider.getAddressOrRevert(AP_CONTRACTS_REGISTER, 1)); - cr.addPool(address(pool)); - cr.addCreditManager(address(cmMock)); + cr.addPool(address(poolMock)); + cr.addCreditManager(address(creditManagerMock)); - vm.label(address(pool), "Pool"); + vm.label(address(poolMock), "Pool"); vm.stopPrank(); } @@ -93,8 +93,8 @@ contract PoolQuotaKeeperUnitTest is TestHelper, BalanceHelper, IPoolQuotaKeeperV // 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"); + assertEq(address(poolMock), pqk.pool(), "Incorrect poolMock address"); + assertEq(underlying, pqk.underlying(), "Incorrect poolMock address"); } // U:[PQK-2]: configuration functions revert if called nonConfigurator(nonController) @@ -110,6 +110,9 @@ contract PoolQuotaKeeperUnitTest is TestHelper, BalanceHelper, IPoolQuotaKeeperV vm.expectRevert(CallerNotControllerException.selector); pqk.setTokenLimit(DUMB_ADDRESS, 1); + vm.expectRevert(CallerNotControllerException.selector); + pqk.setTokenQuotaIncreaseFee(DUMB_ADDRESS, 1); + vm.stopPrank(); } @@ -127,17 +130,18 @@ contract PoolQuotaKeeperUnitTest is TestHelper, BalanceHelper, IPoolQuotaKeeperV } // 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 { + function test_U_PQK_04_creditManagerOnly_funcitons_reverts_if_called_by_non_gauge() public { vm.startPrank(USER); vm.expectRevert(CallerNotCreditManagerException.selector); - pqk.updateQuota(DUMB_ADDRESS, address(1), 0, 0); + pqk.updateQuota(DUMB_ADDRESS, address(1), 0, 0, 0); vm.expectRevert(CallerNotCreditManagerException.selector); pqk.removeQuotas(DUMB_ADDRESS, new address[](1), false); vm.expectRevert(CallerNotCreditManagerException.selector); pqk.accrueQuotaInterest(DUMB_ADDRESS, new address[](1)); + vm.stopPrank(); } @@ -193,14 +197,16 @@ contract PoolQuotaKeeperUnitTest is TestHelper, BalanceHelper, IPoolQuotaKeeperV setUp(); gaugeMock.addQuotaToken(DAI, DAI_QUOTA_RATE); - gaugeMock.addQuotaToken(USDC, USDC_QUOTA_RATE); + vm.prank(address(gaugeMock)); + pqk.updateRates(); + int96 daiQuota; int96 usdcQuota; if (caseIndex == 1) { - pqk.addCreditManager(address(cmMock)); + pqk.addCreditManager(address(creditManagerMock)); pqk.setTokenLimit(DAI, uint96(100_000 * WAD)); pqk.setTokenLimit(USDC, uint96(100_000 * WAD)); @@ -208,17 +214,31 @@ contract PoolQuotaKeeperUnitTest is TestHelper, BalanceHelper, IPoolQuotaKeeperV daiQuota = int96(uint96(100 * WAD)); usdcQuota = int96(uint96(200 * WAD)); - vm.prank(address(cmMock)); - pqk.updateQuota({creditAccount: DUMB_ADDRESS, token: DAI, quotaChange: daiQuota, minQuota: 0}); - - vm.prank(address(cmMock)); - pqk.updateQuota({creditAccount: DUMB_ADDRESS, token: USDC, quotaChange: usdcQuota, minQuota: 0}); + vm.prank(address(creditManagerMock)); + pqk.updateQuota({ + creditAccount: DUMB_ADDRESS, + token: DAI, + quotaChange: daiQuota, + minQuota: 0, + maxQuota: type(uint96).max + }); + + vm.prank(address(creditManagerMock)); + pqk.updateQuota({ + creditAccount: DUMB_ADDRESS, + token: USDC, + quotaChange: usdcQuota, + minQuota: 0, + maxQuota: type(uint96).max + }); } vm.warp(block.timestamp + 365 days); + address[] memory tokens = new address[](2); tokens[0] = DAI; tokens[1] = USDC; + vm.expectCall(address(gaugeMock), abi.encodeCall(IGaugeV3.getRates, tokens)); vm.expectEmit(true, true, false, true); @@ -230,9 +250,10 @@ contract PoolQuotaKeeperUnitTest is TestHelper, BalanceHelper, IPoolQuotaKeeperV uint256 expectedQuotaRevenue = (DAI_QUOTA_RATE * uint96(daiQuota) + USDC_QUOTA_RATE * uint96(usdcQuota)) / PERCENTAGE_FACTOR; - vm.expectCall(address(pool), abi.encodeCall(IPoolV3.setQuotaRevenue, expectedQuotaRevenue)); + vm.expectCall(address(poolMock), abi.encodeCall(IPoolV3.setQuotaRevenue, expectedQuotaRevenue)); - gaugeMock.updateEpoch(); + vm.prank(address(gaugeMock)); + pqk.updateRates(); (uint96 totalQuoted, uint96 limit, uint16 rate, uint192 cumulativeIndexLU_RAY,) = pqk.totalQuotaParams(DAI); @@ -254,13 +275,13 @@ contract PoolQuotaKeeperUnitTest is TestHelper, BalanceHelper, IPoolQuotaKeeperV assertEq(pqk.lastQuotaRateUpdate(), block.timestamp, _testCaseErr("Incorect lastQuotaRateUpdate timestamp")); - assertEq(pool.quotaRevenue(), expectedQuotaRevenue, _testCaseErr("Incorect expectedQuotaRevenue")); + assertEq(poolMock.quotaRevenue(), expectedQuotaRevenue, _testCaseErr("Incorect expectedQuotaRevenue")); } } // U:[PQK-8]: setGauge works as expected function test_U_PQK_08_setGauge_works_as_expected() public { - pqk = new PoolQuotaKeeperV3(address(pool)); + pqk = new PoolQuotaKeeperV3(address(poolMock)); assertEq(pqk.gauge(), address(0), "SETUP: incorrect address at start"); @@ -285,39 +306,39 @@ contract PoolQuotaKeeperUnitTest is TestHelper, BalanceHelper, IPoolQuotaKeeperV // U:[PQK-9]: addCreditManager works as expected function test_U_PQK_09_addCreditManager_reverts_for_non_cm_contract() public { + // Case: non registered credit manager vm.expectRevert(RegisteredCreditManagerOnlyException.selector); pqk.addCreditManager(DUMB_ADDRESS); - cmMock.setPoolService(DUMB_ADDRESS); - + // Case: credit manager with different poolMock address + creditManagerMock.setPoolService(DUMB_ADDRESS); vm.expectRevert(IncompatibleCreditManagerException.selector); - - pqk.addCreditManager(address(cmMock)); + pqk.addCreditManager(address(creditManagerMock)); } // U:[PQK-10]: addCreditManager works as expected function test_U_PQK_10_addCreditManager_works_as_expected() public { - pqk = new PoolQuotaKeeperV3(address(pool)); + pqk = new PoolQuotaKeeperV3(address(poolMock)); address[] memory managers = pqk.creditManagers(); assertEq(managers.length, 0, "SETUP: at least one creditmanager is unexpectedly connected"); vm.expectEmit(true, true, false, false); - emit AddCreditManager(address(cmMock)); + emit AddCreditManager(address(creditManagerMock)); - pqk.addCreditManager(address(cmMock)); + pqk.addCreditManager(address(creditManagerMock)); managers = pqk.creditManagers(); assertEq(managers.length, 1, "Incorrect length of connected managers"); - assertEq(managers[0], address(cmMock), "Incorrect address was added to creditManagerSet"); + assertEq(managers[0], address(creditManagerMock), "Incorrect address was added to creditManagerSet"); // check that funciton works correctly for another one step - pqk.addCreditManager(address(cmMock)); + pqk.addCreditManager(address(creditManagerMock)); managers = pqk.creditManagers(); assertEq(managers.length, 1, "Incorrect length of connected managers"); - assertEq(managers[0], address(cmMock), "Incorrect address was added to creditManagerSet"); + assertEq(managers[0], address(creditManagerMock), "Incorrect address was added to creditManagerSet"); } // U:[PQK-11]: setTokenLimit reverts for unregistered token @@ -342,172 +363,216 @@ contract PoolQuotaKeeperUnitTest is TestHelper, BalanceHelper, IPoolQuotaKeeperV assertEq(limitSet, limit, "Incorrect limit was set"); } - // U:[PQK-13]: updateQuota reverts for unregistered token - function test_U_PQK_13_updateQuotas_reverts_for_unregistered_token() public { - pqk.addCreditManager(address(cmMock)); + // U:[PQK-13]: setTokenQuotaIncreaseFee works as expected + function test_U_PQK_13_setTokenQuotaIncreaseFee_works_as_expected() public { + uint16 fee = 39_99; - address link = tokenTestSuite.addressOf(Tokens.LINK); - vm.expectRevert(TokenIsNotQuotedException.selector); + gaugeMock.addQuotaToken(DUMB_ADDRESS, 11); - vm.prank(address(cmMock)); - pqk.updateQuota({creditAccount: DUMB_ADDRESS, token: link, quotaChange: int96(uint96(100 * WAD)), minQuota: 0}); - } + vm.expectEmit(true, true, false, true); + emit SetQuotaIncreaseFee(DUMB_ADDRESS, fee); - struct QuotaTest { - Tokens token; - int96 change; - uint256 limit; - uint16 rate; - uint256 expectedTotalQuotedAfter; + pqk.setTokenQuotaIncreaseFee(DUMB_ADDRESS, fee); + + (,,,, uint16 feeSet) = pqk.totalQuotaParams(DUMB_ADDRESS); + + assertEq(feeSet, fee, "Incorrect fee was set"); } - struct QuotaTestInAYear { - Tokens token; - int96 change; - uint256 expectedTotalQuotedAfter; + // U:[PQK-14]: updateQuota reverts for unregistered token + function test_U_PQK_14_updateQuotas_reverts_for_unregistered_token() public { + pqk.addCreditManager(address(creditManagerMock)); + + address link = tokenTestSuite.addressOf(Tokens.LINK); + vm.expectRevert(TokenIsNotQuotedException.selector); + + vm.prank(address(creditManagerMock)); + pqk.updateQuota({ + creditAccount: DUMB_ADDRESS, + token: link, + quotaChange: int96(uint96(100 * WAD)), + minQuota: 0, + maxQuota: 1 + }); } - struct UpdateQuotasTestCase { + struct UpdateQuotaTestCase { string name; /// SETUP - uint256 quotaLen; - QuotaTest[2] initialQuotas; - uint256 initialEnabledTokens; + uint256 period; + int96 change; + uint96 minQuota; + uint96 maxQuota; /// expected - int128 expectedQuotaRevenueChange; uint256 expectedCaQuotaInterestChange; - uint256 expectedEnableTokenMaskUpdated; - // In 1 YEAR - QuotaTestInAYear[2] quotasInAYear; - /// expected in 1 YEAR - int128 expectedInAYearQuotaRevenueChange; - uint256 expectedInAYearCaQuotaInterestChange; - uint256 expectedInAYearEnableTokenMaskUpdated; + int96 expectedRealQuotaChange; + bool expectedEnableToken; + bool expectedDisableToken; + bool expectRevert; + int256 expectedQuotaRevenueChange; } - // // 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", - // /// SETUP - // quotaLen: 2, - // initialQuotas: [ - // QuotaTest({token: Tokens.DAI, change: 100, limit: 10_000, rate: 10_00, expectedTotalQuotedAfter: 100}), - // QuotaTest({token: Tokens.USDC, change: 150, limit: 1_000, rate: 20_00, expectedTotalQuotedAfter: 150}) - // ], - // initialEnabledTokens: 0, - // /// expected - // expectedQuotaRevenueChange: 0, - // expectedCaQuotaInterestChange: 0, - // expectedEnableTokenMaskUpdated: 3, - // // In 1 YEAR - // quotasInAYear: [ - // QuotaTestInAYear({token: Tokens.DAI, change: 100, expectedTotalQuotedAfter: 200}), - // QuotaTestInAYear({token: Tokens.USDC, change: -100, expectedTotalQuotedAfter: 50}) - // ], - // expectedInAYearQuotaRevenueChange: 0, - // expectedInAYearCaQuotaInterestChange: 0, - // expectedInAYearEnableTokenMaskUpdated: 3 - // }) - // ]; - // for (uint256 i; i < cases.length; ++i) { - // UpdateQuotasTestCase memory testCase = cases[i]; - - // setUp(); - // vm.startPrank(CONFIGURATOR); - - // pqk.addCreditManager(address(cmMock)); - - // QuotaUpdate[] memory quotaUpdates = new QuotaUpdate[](testCase.quotaLen); - - // for (uint256 j; j < testCase.quotaLen; ++j) { - // address token = tokenTestSuite.addressOf(testCase.initialQuotas[j].token); - // cmMock.addToken(token, 1 << (j)); - // gaugeMock.addQuotaToken(token, testCase.initialQuotas[j].rate); - // pqk.setTokenLimit(token, uint96(testCase.initialQuotas[j].limit)); - - // quotaUpdates[j] = QuotaUpdate({token: token, quotaChange: testCase.initialQuotas[j].change}); - // } - - // vm.stopPrank(); - - // int128 quBefore = int128(pool.quotaRevenue()); - - // /// UPDATE QUOTAS - - // uint256 tokensToEnable; - // uint256 tokensToDisable; - // uint256 caQuotaInterestChange; - // (caQuotaInterestChange, tokensToEnable, tokensToDisable) = cmMock.updateQuotas(DUMB_ADDRESS, quotaUpdates); - - // // assertEq( - // // enableTokenMaskUpdated, - // // testCase.expectedEnableTokenMaskUpdated, - // // _testCaseErr(testCase.name, "Incorrece enable token mask") - // // ); - - // assertEq( - // caQuotaInterestChange, - // testCase.expectedCaQuotaInterestChange, - // _testCaseErr(testCase.name, "Incorrece caQuotaInterestChange") - // ); - - // assertEq( - // quBefore - int128(pool.quotaRevenue()), - // testCase.expectedQuotaRevenueChange, - // _testCaseErr(testCase.name, "Incorrece QuotaRevenueChange") - // ); - - // for (uint256 j; j < testCase.quotaLen; ++j) { - // address token = tokenTestSuite.addressOf(testCase.initialQuotas[j].token); - // (uint96 totalQuoted,,,) = pqk.totalQuotaParams(token); - - // assertEq( - // totalQuoted, - // testCase.initialQuotas[j].expectedTotalQuotedAfter, - // _testCaseErr(testCase.name, "Incorrect expectedTotalQuotedAfter") - // ); - // } - // vm.warp(block.timestamp + 365 days); - - // for (uint256 j; j < testCase.quotaLen; ++j) { - // address token = tokenTestSuite.addressOf(testCase.quotasInAYear[j].token); - - // quotaUpdates[j] = QuotaUpdate({token: token, quotaChange: testCase.quotasInAYear[j].change}); - // } - - // (caQuotaInterestChange, tokensToEnable, tokensToDisable) = cmMock.updateQuotas(DUMB_ADDRESS, quotaUpdates); - - // // TODO: change the test - // // assertEq( - // // enableTokenMaskUpdatedInAYear, - // // testCase.expectedInAYearEnableTokenMaskUpdated, - // // _testCaseErr(testCase.name, "Incorrect enable token mask in a year") - // // ); - - // assertEq( - // caQuotaInterestChange, - // testCase.expectedInAYearCaQuotaInterestChange, - // _testCaseErr(testCase.name, "Incorrect caQuotaInterestChange in a year") - // ); - - // assertEq( - // quBefore - int128(pool.quotaRevenue()), - // testCase.expectedInAYearQuotaRevenueChange, - // _testCaseErr(testCase.name, "Incorrect QuotaRevenueChange in a year") - // ); - - // for (uint256 j; j < testCase.quotaLen; ++j) { - // address token = tokenTestSuite.addressOf(testCase.initialQuotas[j].token); - // (uint96 totalQuoted,,,) = pqk.totalQuotaParams(token); - - // assertEq( - // totalQuoted, - // testCase.quotasInAYear[j].expectedTotalQuotedAfter, - // _testCaseErr(testCase.name, "Incorrect expectedTotalQuotedAfter in a year") - // ); - // } - // } - // } + // U:[PQK-15]: updateQuotas works as expected + function test_U_PQK_15_updateQuotas_works_as_expected() public { + UpdateQuotaTestCase[7] memory cases = [ + UpdateQuotaTestCase({ + name: "Open new quota < limit", + /// SETUP + period: 0, + change: 10_000, + minQuota: 0, + maxQuota: 100_000_000, + /// + expectedCaQuotaInterestChange: 4_000, + expectedRealQuotaChange: 10_000, + expectedEnableToken: true, + expectedDisableToken: false, + expectRevert: false, + expectedQuotaRevenueChange: 10_000 * 10 / 100 // 10% additional rate + }), + UpdateQuotaTestCase({ + name: "Quota in a year", + /// SETUP + period: 365 days, + change: 0, + minQuota: 0, + maxQuota: 100_000_000, + /// 10_000 * 10% quota + expectedCaQuotaInterestChange: 1_000, + expectedRealQuotaChange: 0, + expectedEnableToken: false, + expectedDisableToken: false, + expectRevert: false, + expectedQuotaRevenueChange: 0 + }), + UpdateQuotaTestCase({ + name: "Quota < minQuota", + /// SETUP + period: 0, + change: 0, + minQuota: 11_000, + maxQuota: 100_000_000, + /// 10_000 * 10% quota + expectedCaQuotaInterestChange: 0, + expectedRealQuotaChange: 0, + expectedEnableToken: false, + expectedDisableToken: false, + expectRevert: true, + expectedQuotaRevenueChange: 0 + }), + UpdateQuotaTestCase({ + name: "Quota > maxQuota", + /// SETUP + period: 0, + change: 0, + minQuota: 0, + maxQuota: 9_000, + /// 10_000 * 10% quota + expectedCaQuotaInterestChange: 0, + expectedRealQuotaChange: 0, + expectedEnableToken: false, + expectedDisableToken: false, + expectRevert: true, + expectedQuotaRevenueChange: 0 + }), + UpdateQuotaTestCase({ + name: "Quota reduction < minQuota, quota > minQuota", + /// SETUP + period: 365 days, + change: -5_000, + minQuota: 1_000, + maxQuota: 100_000_000, + /// 10_000 * 10% quota + expectedCaQuotaInterestChange: 1_000, + expectedRealQuotaChange: -5000, + expectedEnableToken: false, + expectedDisableToken: false, + expectRevert: false, + expectedQuotaRevenueChange: (-5_000 * 10 / 100) + }), + UpdateQuotaTestCase({ + name: "Quota > limit", + /// SETUP + period: 365 days, + change: 100_000, + minQuota: 1_000, + maxQuota: 100_000_000, + /// 10_000 * 10% quota + expectedCaQuotaInterestChange: 500 + 35_000 * 40 / 100, // 500 for prev year + fee + expectedRealQuotaChange: 35_000, + expectedEnableToken: false, + expectedDisableToken: false, + expectRevert: false, + expectedQuotaRevenueChange: (35_000 * 10 / 100) + }), + UpdateQuotaTestCase({ + name: "Quota disable token is fully paid", + /// SETUP + period: 365 days, + change: -40_000, + minQuota: 0, + maxQuota: 100_000_000, + expectedCaQuotaInterestChange: 40_000 * 10 / 100, // 4_000 for prev year + expectedRealQuotaChange: -40_000, + expectedEnableToken: false, + expectedDisableToken: true, + expectRevert: false, + expectedQuotaRevenueChange: (-40_000 * 10 / 100) + }) + ]; + + pqk.addCreditManager(address(creditManagerMock)); + + address token = makeAddr("TOKEN"); + + address creditAccount = makeAddr("CREDIT_ACCOUNT"); + + gaugeMock.addQuotaToken({token: token, rate: 10_00}); // 10% rate + + vm.prank(address(gaugeMock)); + pqk.updateRates(); + pqk.setTokenLimit({token: token, limit: 40_000}); // 40_000 max + pqk.setTokenQuotaIncreaseFee({token: token, fee: 40_00}); // 40% + + for (uint256 i; i < cases.length; ++i) { + UpdateQuotaTestCase memory _case = cases[i]; + + caseName = _case.name; + + vm.warp(block.timestamp + _case.period); + + /// UPDATE QUOTA + + if (_case.expectRevert) { + vm.expectRevert(QuotaIsOutOfBoundsException.selector); + } else { + if (_case.expectedQuotaRevenueChange != 0) { + vm.expectCall( + address(poolMock), + abi.encodeCall(IPoolV3.updateQuotaRevenue, (_case.expectedQuotaRevenueChange)) + ); + } + } + + vm.prank(address(creditManagerMock)); + (uint256 caQuotaInterestChange, int96 realQuotaChange, bool enableToken, bool disableToken) = + pqk.updateQuota(creditAccount, token, _case.change, _case.minQuota, _case.maxQuota); + + if (!_case.expectRevert) { + assertEq( + caQuotaInterestChange, + _case.expectedCaQuotaInterestChange, + _testCaseErr("Incorrece caQuotaInterestChange") + ); + + assertEq( + realQuotaChange, _case.expectedRealQuotaChange, _testCaseErr("Incorrece expectedRealQuotaChang") + ); + + assertEq(enableToken, _case.expectedEnableToken, _testCaseErr("Incorrece enableToken")); + + assertEq(disableToken, _case.expectedDisableToken, _testCaseErr("Incorrece disableToken")); + } + } + } } diff --git a/contracts/test/unit/pool/PoolV3.t.sol b/contracts/test/unit/pool/PoolV3.unit.t.sol similarity index 100% rename from contracts/test/unit/pool/PoolV3.t.sol rename to contracts/test/unit/pool/PoolV3.unit.t.sol