From 0a8a3c8b1f79b58fb332b61ad5ca9975f5cbaa25 Mon Sep 17 00:00:00 2001 From: Mikael <26343374+0xmikko@users.noreply.github.com> Date: Sat, 20 May 2023 00:04:23 +0200 Subject: [PATCH] fix: comutation for DEBT_ONLY covered --- contracts/credit/CreditManagerV3.sol | 119 +++---- contracts/interfaces/ICreditManagerV3.sol | 2 +- contracts/interfaces/IPoolQuotaKeeper.sol | 2 +- contracts/libraries/CreditLogic.sol | 26 +- contracts/pool/PoolQuotaKeeper.sol | 2 +- contracts/test/lib/helper.sol | 69 ++++ .../test/mocks/pool/PoolQuotaKeeperMock.sol | 14 +- contracts/test/suites/TokensTestSuite.sol | 26 ++ .../test/unit/credit/CreditManagerV3.t.sol | 297 +++++++++++++++++- .../unit/credit/CreditManagerV3Harness.sol | 20 +- 10 files changed, 478 insertions(+), 99 deletions(-) diff --git a/contracts/credit/CreditManagerV3.sol b/contracts/credit/CreditManagerV3.sol index 274fadd4..948757ae 100644 --- a/contracts/credit/CreditManagerV3.sol +++ b/contracts/credit/CreditManagerV3.sol @@ -656,34 +656,38 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT if (task == CollateralCalcTask.GENERIC_PARAMS) return collateralDebtData; // U:[CM-20] /// /// COMPUTE DEBT PARAMS - collateralDebtData.enabledTokensMask = enabledTokensMask; + collateralDebtData.enabledTokensMask = enabledTokensMask; // U:[CM-21] if (supportsQuotas) { - collateralDebtData.quotedTokenMask = quotedTokenMask & collateralDebtData.enabledTokensMask; - collateralDebtData._poolQuotaKeeper = poolQuotaKeeper(); + collateralDebtData._poolQuotaKeeper = poolQuotaKeeper(); // U:[CM-21] ( collateralDebtData.quotedTokens, collateralDebtData.cumulativeQuotaInterest, collateralDebtData.quotas, collateralDebtData.quotedLts, - collateralDebtData.quotedTokenMask - ) = _getQuotaTokenData({ + collateralDebtData.enabledQuotedTokenMask + ) = _getQuotedTokensData({ creditAccount: creditAccount, enabledTokensMask: enabledTokensMask, _poolQuotaKeeper: collateralDebtData._poolQuotaKeeper - }); + }); // U:[CM-21] - collateralDebtData.cumulativeQuotaInterest += creditAccountInfo[creditAccount].cumulativeQuotaInterest - 1; + collateralDebtData.cumulativeQuotaInterest += creditAccountInfo[creditAccount].cumulativeQuotaInterest - 1; // U:[CM-21] } - (collateralDebtData.accruedInterest, collateralDebtData.accruedFees) = - collateralDebtData.calcAccruedInterestAndFees({feeInterest: feeInterest}); + 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; + if (task == CollateralCalcTask.DEBT_ONLY) return collateralDebtData; // U:[CM-21] /// /// COMPUTES COLLATERAL - /// If task == FULL_COLLATERAL_CHECK_LAZY, until it finds enough collateral + /// If task == FULL_COLLATERAL_CHECK_LAZY, collateral is computed until it less enough address _priceOracle = priceOracle; collateralDebtData.totalDebtUSD = _convertToUSD({ @@ -726,6 +730,53 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT } } + function _getQuotedTokensData(address creditAccount, uint256 enabledTokensMask, address _poolQuotaKeeper) + internal + view + returns ( + address[] memory quotaTokens, + uint256 outstandingQuotaInterest, + uint256[] memory quotas, + uint16[] memory lts, + uint256 quotedMask + ) + { + uint256 _maxEnabledTokens = maxEnabledTokens; + + quotedMask = enabledTokensMask & quotedTokenMask; + + if (quotedMask != 0) { + quotaTokens = new address[](_maxEnabledTokens); + quotas = new uint256[](_maxEnabledTokens); + lts = new uint16[](_maxEnabledTokens); + + uint256 j; + unchecked { + for (uint256 tokenMask = 2; tokenMask <= quotedMask; tokenMask <<= 1) { + if (j == _maxEnabledTokens) { + revert TooManyEnabledTokensException(); + } + + if (quotedMask & tokenMask != 0) { + address token; + (token, lts[j]) = _collateralTokensByMask({tokenMask: tokenMask, calcLT: true}); + + quotaTokens[j] = token; + + uint256 outstandingInterestDelta; + (quotas[j], outstandingInterestDelta) = + _getQuotaAndOutstandingInterest(_poolQuotaKeeper, creditAccount, token); + + // Safe because quotaInterest = (quota is uint96) * APY * time, so even with 1000% APY, it will take 10**10 years for overflow + outstandingQuotaInterest += outstandingInterestDelta; + + ++j; + } + } + } + } + } + // // QUOTAS MANAGEMENT // @@ -1287,7 +1338,7 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT view returns (uint256 quoted, uint256 outstandingInterest) { - return IPoolQuotaKeeper(_poolQuotaKeeper).getQuotaAndInterest(creditAccount, token); + return IPoolQuotaKeeper(_poolQuotaKeeper).getQuotaAndOutstandingInterest(creditAccount, token); } function _convertToUSD(address _priceOracle, uint256 amountInToken, address token) @@ -1305,48 +1356,4 @@ contract CreditManagerV3 is ICreditManagerV3, SanityCheckTrait, ReentrancyGuardT { amountInToken = IPriceOracleV2(_priceOracle).convertFromUSD(amountInUSD, token); } - - function _getQuotaTokenData(address creditAccount, uint256 enabledTokensMask, address _poolQuotaKeeper) - internal - view - returns ( - address[] memory quotaTokens, - uint256 outstandingQuotaInterest, - uint256[] memory quotas, - uint16[] memory lts, - uint256 quotedMask - ) - { - uint256 _maxEnabledTokens = maxEnabledTokens; - - quotedMask = enabledTokensMask & quotedTokenMask; - quotaTokens = new address[](_maxEnabledTokens); - quotas = new uint256[](_maxEnabledTokens); - lts = new uint16[](_maxEnabledTokens); - - uint256 j; - unchecked { - for (uint256 tokenMask = 2; tokenMask <= quotedMask; tokenMask <<= 1) { - if (quotedMask & tokenMask != 0) { - address token; - (token, lts[j]) = _collateralTokensByMask(tokenMask, true); - - quotaTokens[j] = token; - - uint256 outstandingInterestDelta; - (quotas[j], outstandingInterestDelta) = - _getQuotaAndOutstandingInterest(_poolQuotaKeeper, creditAccount, token); - - // Safe because quotaInterest = (quota is uint96) * APY * time, so even with 1000% APY, it will take 10**10 years for overflow - outstandingQuotaInterest += outstandingInterestDelta; - - ++j; - - if (j >= _maxEnabledTokens) { - revert TooManyEnabledTokensException(); - } - } - } - } - } } diff --git a/contracts/interfaces/ICreditManagerV3.sol b/contracts/interfaces/ICreditManagerV3.sol index ce9d3706..062e2cbc 100644 --- a/contracts/interfaces/ICreditManagerV3.sol +++ b/contracts/interfaces/ICreditManagerV3.sol @@ -53,7 +53,7 @@ struct CollateralDebtData { uint256 totalValueUSD; uint256 twvUSD; uint256 enabledTokensMask; - uint256 quotedTokenMask; + uint256 enabledQuotedTokenMask; address[] quotedTokens; uint16[] quotedLts; uint256[] quotas; diff --git a/contracts/interfaces/IPoolQuotaKeeper.sol b/contracts/interfaces/IPoolQuotaKeeper.sol index 2cae5714..ed04b208 100644 --- a/contracts/interfaces/IPoolQuotaKeeper.sol +++ b/contracts/interfaces/IPoolQuotaKeeper.sol @@ -95,7 +95,7 @@ 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 getQuotaAndInterest(address creditAccount, address token) + function getQuotaAndOutstandingInterest(address creditAccount, address token) external view returns (uint256 quoted, uint256 outstandingInterest); diff --git a/contracts/libraries/CreditLogic.sol b/contracts/libraries/CreditLogic.sol index 56080502..dcd97fdf 100644 --- a/contracts/libraries/CreditLogic.sol +++ b/contracts/libraries/CreditLogic.sol @@ -269,29 +269,6 @@ library CreditLogic { // // COLLATERAL & DEBT COMPUTATION // - - /// @dev IMPLEMENTATION: calcAccruedInterestAndFees - // / @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 calcAccruedInterestAndFees(CollateralDebtData memory collateralDebtData, uint16 feeInterest) - internal - pure - returns (uint256 accruedInterest, uint256 accruedFees) - { - // 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 = calcAccruedInterest( - collateralDebtData.debt, collateralDebtData.cumulativeIndexLastUpdate, collateralDebtData.cumulativeIndexNow - ) + collateralDebtData.cumulativeQuotaInterest; // F:[CM-49] - - // Fees are computed as a percentage of interest - accruedFees = collateralDebtData.accruedInterest * feeInterest / PERCENTAGE_FACTOR; // F: [CM-49] - } - function calcCollateral( CollateralDebtData memory collateralDebtData, address creditAccount, @@ -326,7 +303,8 @@ library CreditLogic { } } { - uint256 tokensToCheckMask = collateralDebtData.enabledTokensMask.disable(collateralDebtData.quotedTokenMask); + uint256 tokensToCheckMask = + collateralDebtData.enabledTokensMask.disable(collateralDebtData.enabledQuotedTokenMask); uint256 tvDelta; uint256 twvDelta; diff --git a/contracts/pool/PoolQuotaKeeper.sol b/contracts/pool/PoolQuotaKeeper.sol index f6ae5b51..28478bab 100644 --- a/contracts/pool/PoolQuotaKeeper.sol +++ b/contracts/pool/PoolQuotaKeeper.sol @@ -181,7 +181,7 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait, ContractsReg // // GETTERS // - function getQuotaAndInterest(address creditAccount, address token) + function getQuotaAndOutstandingInterest(address creditAccount, address token) external view override diff --git a/contracts/test/lib/helper.sol b/contracts/test/lib/helper.sol index 94bc0f22..4ca2e515 100644 --- a/contracts/test/lib/helper.sol +++ b/contracts/test/lib/helper.sol @@ -17,4 +17,73 @@ contract TestHelper is Test { 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/pool/PoolQuotaKeeperMock.sol b/contracts/test/mocks/pool/PoolQuotaKeeperMock.sol index 4d524ef1..aa751834 100644 --- a/contracts/test/mocks/pool/PoolQuotaKeeperMock.sol +++ b/contracts/test/mocks/pool/PoolQuotaKeeperMock.sol @@ -43,6 +43,9 @@ contract PoolQuotaKeeperMock is IPoolQuotaKeeper { 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; @@ -80,15 +83,20 @@ contract PoolQuotaKeeperMock is IPoolQuotaKeeper { /// @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 getQuotaAndInterest(address creditAccount, address token) + function getQuotaAndOutstandingInterest(address creditAccount, address token) external view override returns (uint256 quoted, uint256 interest) { - quoted = return_quoted; - interest = return_interest; + quoted = _quoted[token]; + interest = _outstandingInterest[token]; } /// @dev Returns cumulative index in RAY for a quoted token. Returns 0 for non-quoted tokens. diff --git a/contracts/test/suites/TokensTestSuite.sol b/contracts/test/suites/TokensTestSuite.sol index f1aa506b..ae8a3fd2 100644 --- a/contracts/test/suites/TokensTestSuite.sol +++ b/contracts/test/suites/TokensTestSuite.sol @@ -107,4 +107,30 @@ contract TokensTestSuite is Test, TokensData, TokensTestSuiteHelper { function burn(Tokens t, address from, uint256 amount) external { burn(addressOf[t], from, amount); } + + function listOf(Tokens t1) external returns (address[] memory tokensList) { + tokensList = new address[](1); + tokensList[0] = addressOf[t1]; + } + + function listOf(Tokens t1, Tokens t2) external 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 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 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/unit/credit/CreditManagerV3.t.sol b/contracts/test/unit/credit/CreditManagerV3.t.sol index 45ad0be9..0b306299 100644 --- a/contracts/test/unit/credit/CreditManagerV3.t.sol +++ b/contracts/test/unit/credit/CreditManagerV3.t.sol @@ -212,13 +212,45 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH return IERC20Metadata(token).decimals(); } - function _addTokens(address creditAccount, uint8 numberOfTokens, uint256 balance) internal { + function _addToken(Tokens token, uint16 lt) internal { + _addToken(tokenTestSuite.addressOf(token), lt); + } + + function _addToken(address token, uint16 lt) internal { + creditManager.addToken({token: token}); + 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); - creditManager.addToken(address(t)); - creditManager.setCollateralTokenData(address(t), 8000, 8000, type(uint40).max, 0); + _addToken({token: address(t), lt: 80_00}); t.mint(creditAccount, balance * ((i + 2) % 5)); @@ -228,6 +260,10 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH } } + function _getTokenMaskOrRevert(Tokens token) internal view returns (uint256) { + return creditManager.getTokenMaskOrRevert(tokenTestSuite.addressOf(token)); + } + /// /// /// TESTS @@ -870,7 +906,7 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH { uint256 randomAmount = skipTokenMask % DAI_ACCOUNT_AMOUNT; tokenTestSuite.mint({token: weth, to: creditAccount, amount: randomAmount}); - _addTokens({creditAccount: creditAccount, numberOfTokens: numberOfTokens, balance: randomAmount}); + _addTokensBatch({creditAccount: creditAccount, numberOfTokens: numberOfTokens, balance: randomAmount}); } string memory caseName = string.concat("token transfer with ", Strings.toString(numberOfTokens), " on account"); @@ -1378,7 +1414,7 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH /// @notice sets price 1 USD for underlying priceOracleMock.setPrice(underlying, 10 ** 8); - _addTokens({creditAccount: creditAccount, numberOfTokens: numberOfTokens, balance: amount}); + _addTokensBatch({creditAccount: creditAccount, numberOfTokens: numberOfTokens, balance: amount}); creditManager.setCreditAccountInfoMap({ creditAccount: creditAccount, @@ -1501,5 +1537,254 @@ contract CreditManagerV3UnitTest is TestHelper, ICreditManagerV3Events, BalanceH } /// @dev U:[CM-21]: calcDebtAndCollateral works correctly for DEBT_ONLY task - function test_U_CM_21_calcDebtAndCollateral_works_correctly_for_DEBT_ONLY_task() public withoutSupportQuotas {} + 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}); + + uint256 cumulativeIndexNow = RAY * 22 / 10; + uint256 cumulativeIndexLastUpdate = RAY * 21 / 10; + + poolMock.setCumulativeIndexNow(cumulativeIndexNow); + + if (supportsQuotas) { + creditManager.setQuotedMask(LINK_TOKEN_MASK | STETH_TOKEN_MASK); + } + + creditManager.setCreditAccountInfoMap({ + creditAccount: creditAccount, + debt: debt, + cumulativeIndexLastUpdate: cumulativeIndexLastUpdate, + cumulativeQuotaInterest: INITIAL_INTEREST, + enabledTokensMask: UNDERLYING_TOKEN_MASK | LINK_TOKEN_MASK | STETH_TOKEN_MASK, + flags: 0, + borrower: USER + }); + + poolMock.setCumulativeIndexNow(cumulativeIndexNow); + 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.enabledQuotedTokenMask, + supportsQuotas ? LINK_TOKEN_MASK | STETH_TOKEN_MASK : 0, + "Incorrect quotedLts" + ); + + assertEq( + collateralDebtData.accruedInterest, + CreditLogic.calcAccruedInterest({ + amount: debt, + cumulativeIndexLastUpdate: cumulativeIndexLastUpdate, + cumulativeIndexNow: cumulativeIndexNow + }) + (supportsQuotas ? LINK_INTEREST + STETH_INTEREST + (INITIAL_INTEREST - 1) : 0), + "Incorrect accruedInterest" + ); + + assertEq( + collateralDebtData.accruedFees, + collateralDebtData.accruedInterest * DEFAULT_FEE_INTEREST / PERCENTAGE_FACTOR, + "Incorrect accruedFees" + ); + } + + /// + /// GET QUOTED TOKENS DATA + /// + + struct GetQuotedTokenDataTestCase { + string name; + // + uint256 enabledTokensMask; + address[] expectedQuotaTokens; + uint256 expertedOutstandingQuotaInterest; + uint256[] expectedQuotas; + uint16[] expectedLts; + uint256 expectedQuotedMask; + bool expectRevert; + } + + /// @dev U:[CM-31]: _getQuotedTokensData works correctly + function test_U_CM_31_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}); + + creditManager.setQuotedMask(LINK_TOKEN_MASK | USDT_TOKEN_MASK | STETH_TOKEN_MASK | CVX_TOKEN_MASK); + creditManager.setMaxEnabledTokens(3); + + // + // 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), + expectedQuotedMask: 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), + expectedQuotedMask: 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), + expectedQuotedMask: STETH_TOKEN_MASK, + 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), + expectedQuotedMask: LINK_TOKEN_MASK | STETH_TOKEN_MASK, + 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), + expectedQuotedMask: LINK_TOKEN_MASK | STETH_TOKEN_MASK | CVX_TOKEN_MASK, + expectRevert: false + }) + ]; + + for (uint256 i; i < cases.length; ++i) { + uint256 snapshot = vm.snapshot(); + + GetQuotedTokenDataTestCase memory _case = cases[i]; + + string memory 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 quotedMask + ) = creditManager.getQuotedTokensData({ + creditAccount: DUMB_ADDRESS, + enabledTokensMask: _case.enabledTokensMask, + _poolQuotaKeeper: address(poolQuotaKeeperMock) + }); + + if (!_case.expectRevert) { + assertEq(quotaTokens, _case.expectedQuotaTokens, _testCaseErr(caseName, "Incorrect quotedTokens")); + assertEq( + outstandingQuotaInterest, + _case.expertedOutstandingQuotaInterest, + _testCaseErr(caseName, "Incorrect expertedOutstandingQuotaInterest") + ); + assertEq(quotas, _case.expectedQuotas, _testCaseErr(caseName, "Incorrect expectedQuotas")); + assertEq(lts, _case.expectedLts, _testCaseErr(caseName, "Incorrect expectedLts")); + assertEq(quotedMask, _case.expectedQuotedMask, _testCaseErr(caseName, "Incorrect expectedQuotedMask")); + } + + vm.revertTo(snapshot); + } + } } diff --git a/contracts/test/unit/credit/CreditManagerV3Harness.sol b/contracts/test/unit/credit/CreditManagerV3Harness.sol index b69620b1..33436141 100644 --- a/contracts/test/unit/credit/CreditManagerV3Harness.sol +++ b/contracts/test/unit/credit/CreditManagerV3Harness.sol @@ -101,15 +101,21 @@ contract CreditManagerV3Harness is CreditManagerV3 { return _hasWithdrawals(creditAccount); } - // function calcCancellableWithdrawalsValue(address creditAccount, bool isForceCancel) external { - // _calcCancellableWithdrawalsValue(creditAccount, isForceCancel); - // } - function saveEnabledTokensMask(address creditAccount, uint256 enabledTokensMask) external { _saveEnabledTokensMask(creditAccount, enabledTokensMask); } - // function convertToUSD(uint256 amountInToken, address token) external returns (uint256 amountInUSD) { - // return _convertToUSD(amountInToken, token); - // } + 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); + } }