diff --git a/contracts/credit/CreditFacadeV3.sol b/contracts/credit/CreditFacadeV3.sol index 767cd72a..f9514fd2 100644 --- a/contracts/credit/CreditFacadeV3.sol +++ b/contracts/credit/CreditFacadeV3.sol @@ -72,7 +72,7 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { using SafeERC20 for IERC20; /// @notice Contract version - uint256 public constant override version = 3_00; + uint256 public constant override version = 3_01; /// @notice Maximum quota size, as a multiple of `maxDebt` uint256 public constant override maxQuotaMultiplier = 2; @@ -824,7 +824,9 @@ contract CreditFacadeV3 is ICreditFacadeV3, ACLNonReentrantTrait { (tokensToEnable, tokensToDisable) = ICreditManagerV3(creditManager).updateQuota({ creditAccount: creditAccount, token: token, - quotaChange: quotaChange, + quotaChange: quotaChange != type(int96).min + ? quotaChange / int96(uint96(PERCENTAGE_FACTOR)) * int96(uint96(PERCENTAGE_FACTOR)) + : quotaChange, minQuota: minQuota, maxQuota: uint96(Math.min(type(uint96).max, maxQuotaMultiplier * debtLimits.maxDebt)) }); // U:[FA-34] diff --git a/contracts/test/integration/credit/Quotas.int.t.sol b/contracts/test/integration/credit/Quotas.int.t.sol index b9db7d43..ce702d5e 100644 --- a/contracts/test/integration/credit/Quotas.int.t.sol +++ b/contracts/test/integration/credit/Quotas.int.t.sol @@ -408,8 +408,8 @@ contract QuotasIntegrationTest is IntegrationTestHelper, ICreditManagerV3Events (uint16 feeInterest,,,,) = creditManager.fees(); - quotaLink = quotaLink > uint96(100_000 * WAD) ? uint96(100_000 * WAD) : quotaLink; - quotaUsdt = quotaUsdt > uint96(100_000 * WAD) ? uint96(100_000 * WAD) : quotaUsdt; + quotaLink = quotaLink > uint96(100_000 * WAD) ? uint96(100_000 * WAD) : quotaLink / 10_000 * 10_000; + quotaUsdt = quotaUsdt > uint96(100_000 * WAD) ? uint96(100_000 * WAD) : quotaUsdt / 10_000 * 10_000; uint256 expectedTotalDebt = (borrowedAmount * cumulativeIndexAtClose) / cumulativeIndexLastUpdate; expectedTotalDebt += (quotaLink * 1000) / PERCENTAGE_FACTOR; diff --git a/contracts/test/invaritants/TargetAttacker.sol b/contracts/test/invaritants/TargetAttacker.sol new file mode 100644 index 00000000..6f567db2 --- /dev/null +++ b/contracts/test/invaritants/TargetAttacker.sol @@ -0,0 +1,116 @@ +// // SPDX-License-Identifier: UNLICENSED +// // Gearbox Protocol. Generalized leverage for DeFi protocols +// // (c) Gearbox Foundation, 2023. +// pragma solidity ^0.8.17; + +// import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +// import {ICreditManagerV3, CollateralCalcTask, CollateralDebtData} from "../../interfaces/ICreditManagerV3.sol"; +// import {IPriceOracleV3} from "../../interfaces/IPriceOracleV3.sol"; +// import {ITokenTestSuite} from "../interfaces/ITokenTestSuite.sol"; +// import {PriceFeedMock} from "../mocks/oracles/PriceFeedMock.sol"; +// import {Random} from "./Random.sol"; + +// /// @title Target Hacker +// /// This contract simulates different technics to hack the system by provided seed + +// contract TargetAttacker is Random { +// using Math for uint256; + +// ICreditManagerV3 creditManager; +// IPriceOracleV3 priceOracle; +// ITokenTestSuite tokenTestSuite; +// address creditAccount; + +// constructor(address _creditManager, address _priceOracle, address _tokenTestSuite) { +// creditManager = ICreditManagerV3(_creditManager); +// priceOracle = IPriceOracleV3(_priceOracle); +// tokenTestSuite = ITokenTestSuite(_tokenTestSuite); +// } + +// // Act function tests different scenarios related to any action +// // which could potential attacker use. Calling internal contracts +// // depositing funds into pools, withdrawing, liquidating, etc. + +// // it also could update prices for updatable price oracles + +// function act(uint256 _seed) external { +// setSeed(_seed); +// creditAccount = msg.sender; + +// function ()[3] memory fnActions = [_stealTokens, _changeTokenPrice, _swapTokens]; + +// fnActions[getRandomInRange(fnActions.length)](); +// } + +// function _changeTokenPrice() internal { +// uint256 cTokensQty = creditManager.collateralTokensCount(); +// uint256 mask = 1 << getRandomInRange(cTokensQty); +// (address token,) = creditManager.collateralTokenByMask(mask); + +// address priceFeed = IPriceOracleV3(priceOracle).priceFeeds(token); + +// (, int256 price,,,) = PriceFeedMock(priceFeed).latestRoundData(); + +// uint256 sign = getRandomInRange(2); +// uint256 deltaPct = getRandomInRange(500); + +// int256 newPrice = +// sign == 1 ? price * (10000 + int256(deltaPct)) / 10000 : price * (10000 - int256(deltaPct)) / 10000; + +// PriceFeedMock(priceFeed).setPrice(newPrice); +// } + +// function _swapTokens() internal { +// uint256 cTokensQty = creditManager.collateralTokensCount(); +// uint256 mask0 = 1 << getRandomInRange(cTokensQty); +// uint256 mask1 = 1 << getRandomInRange(cTokensQty); + +// (address tokenIn,) = creditManager.collateralTokenByMask(mask0); +// (address tokenOut,) = creditManager.collateralTokenByMask(mask1); + +// uint256 balance = IERC20(tokenIn).balanceOf(creditAccount); + +// uint256 tokenInAmount = getRandomInRange(balance); + +// uint256 tokenInEq = priceOracle.convert(tokenInAmount, tokenIn, tokenOut); + +// IERC20(tokenIn).transferFrom(creditAccount, address(this), tokenInAmount); +// tokenTestSuite.mint(tokenOut, creditAccount, getRandomInRange(tokenInEq)); +// } + +// function _stealTokens() internal { +// uint256 cTokensQty = creditManager.collateralTokensCount(); +// uint256 mask = 1 << getRandomInRange(cTokensQty); +// (tokenIn,) = creditManager.collateralTokenByMask(mask); +// uint256 balance = IERC20(tokenIn).balanceOf(creditAccount); +// IERC20(tokenIn).transferFrom(creditAccount, address(this), getRandomInRange(balance)); +// } + +// /// Swaps token with some deviation from oracle price + +// function _swap() internal { +// uint256 cTokensQty = creditManager.collateralTokensCount(); + +// (tokenIn,) = creditManager.collateralTokenByMask(1 << getRandomInRange(cTokensQty)); +// uint256 balance = IERC20(tokenIn).balanceOf(creditAccount); +// uint256 amount = getRandomInRange(balance); +// IERC20(tokenIn).transferFrom(creditAccount, address(this), amount); + +// (tokenOut,) = creditManager.collateralTokenByMask(1 << getRandomInRange(cTokensQty)); + +// uint256 amountOut = (priceOracle.convert(amount, tokenIn, tokenOut) * (120 - getRandomInRange(40))) / 100; +// amountOut = Math.min(amountOut, IERC20(tokenOut).balanceOf(address(this))); +// IERC20(tokenOut).transfer(creditAccount, amountOut); +// } + +// function _deposit() internal { +// uint256 amount = getRandomInRange95(pool.availableLiquidity()); +// pool.deposit(amount, address(this)); +// } + +// function _withdraw() internal { +// uint256 amount = getRandomInRange95(pool.balanceOf(address(this))); +// pool.withdraw(amount, address(this), address(this)); +// } +// } diff --git a/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol b/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol index b5fb26f9..ab25bdb1 100644 --- a/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol +++ b/contracts/test/unit/credit/CreditFacadeV3.unit.t.sol @@ -1515,7 +1515,7 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve uint256 maskToEnable = 1 << 4; uint256 maskToDisable = 1 << 7; - int96 change = -990; + int96 change = -19900; creditManagerMock.setUpdateQuota({tokensToEnable: maskToEnable, tokensToDisable: maskToDisable}); @@ -1523,7 +1523,7 @@ contract CreditFacadeV3UnitTest is TestHelper, BalanceHelper, ICreditFacadeV3Eve address(creditManagerMock), abi.encodeCall( ICreditManagerV3.updateQuota, - (creditAccount, link, change, 0, uint96(maxDebt * creditFacade.maxQuotaMultiplier())) + (creditAccount, link, change / 10_000 * 10_000, 0, uint96(maxDebt * creditFacade.maxQuotaMultiplier())) ) );