|
| 1 | +// SPDX-License-Identifier: UNLICENSED |
| 2 | +// Gearbox Protocol. Generalized leverage for DeFi protocols |
| 3 | +// (c) Gearbox Foundation, 2023. |
| 4 | +pragma solidity ^0.8.17; |
| 5 | + |
| 6 | +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; |
| 7 | +import {Test} from "forge-std/Test.sol"; |
| 8 | +import {DataCompressorV3} from "../../data/DataCompressorV3.sol"; |
| 9 | +import {MarketCompressor} from "../../compressors/MarketCompressor.sol"; |
| 10 | +import {CreditAccountCompressor} from "../../compressors/CreditAccountCompressor.sol"; |
| 11 | +import {MarketData} from "../../types/MarketData.sol"; |
| 12 | +import {CreditAccountData as CreditAccountDataOld, TokenBalance} from "../../data/Types.sol"; |
| 13 | +import {CreditAccountData, TokenInfo} from "../../types/CreditAccountState.sol"; |
| 14 | +import {MarketFilter, CreditAccountFilter} from "../../types/Filters.sol"; |
| 15 | +import {PriceUpdate} from "@gearbox-protocol/core-v3/contracts/interfaces/IPriceOracleV3.sol"; |
| 16 | +import {RedstonePriceFeed} from "@gearbox-protocol/oracles-v3/contracts/oracles/updatable/RedstonePriceFeed.sol"; |
| 17 | +import {BaseParams} from "../../types/BaseState.sol"; |
| 18 | +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; |
| 19 | +import "forge-std/console.sol"; |
| 20 | + |
| 21 | +contract CreditAccountsEquivalenceTest is Test { |
| 22 | + using Address for address; |
| 23 | + |
| 24 | + DataCompressorV3 public dc3; |
| 25 | + MarketCompressor public mc; |
| 26 | + CreditAccountCompressor public cac; |
| 27 | + |
| 28 | + function _updateAllPriceFeeds() internal { |
| 29 | + MarketFilter memory filter = |
| 30 | + MarketFilter({configurators: new address[](0), pools: new address[](0), underlying: address(0)}); |
| 31 | + |
| 32 | + BaseParams[] memory updatablePriceFeeds = mc.getUpdatablePriceFeeds(filter); |
| 33 | + for (uint256 i = 0; i < updatablePriceFeeds.length; i++) { |
| 34 | + _refreshUpdatablePriceFeed(updatablePriceFeeds[i].addr, updatablePriceFeeds[i].contractType); |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + function test_CAE_01_CreditAccounts_equivalence() public { |
| 39 | + address marketCompressor = vm.envOr("MARKET_COMPRESSOR", address(0)); |
| 40 | + address dataCompressor = vm.envOr("DATA_COMPRESSOR", address(0)); |
| 41 | + address creditAccountCompressor = vm.envOr("CREDIT_ACCOUNT_COMPRESSOR", address(0)); |
| 42 | + address creditManager = vm.envOr("CREDIT_MANAGER", address(0)); |
| 43 | + |
| 44 | + if ( |
| 45 | + marketCompressor == address(0) || dataCompressor == address(0) || creditAccountCompressor == address(0) |
| 46 | + || creditManager == address(0) || !marketCompressor.isContract() || !dataCompressor.isContract() |
| 47 | + || !creditAccountCompressor.isContract() || !creditManager.isContract() |
| 48 | + ) { |
| 49 | + console.log( |
| 50 | + "MarketCompressor, DataCompressor, CreditAccountCompressor, or CreditManager not set, or not a contract. Skipping credit accounts equivalence test." |
| 51 | + ); |
| 52 | + return; |
| 53 | + } |
| 54 | + |
| 55 | + mc = MarketCompressor(marketCompressor); |
| 56 | + dc3 = DataCompressorV3(dataCompressor); |
| 57 | + cac = CreditAccountCompressor(creditAccountCompressor); |
| 58 | + |
| 59 | + // Update all price feeds before running the test |
| 60 | + _updateAllPriceFeeds(); |
| 61 | + |
| 62 | + // Get credit accounts from both implementations |
| 63 | + (CreditAccountData[] memory newCAs,) = cac.getCreditAccounts( |
| 64 | + creditManager, |
| 65 | + CreditAccountFilter({ |
| 66 | + owner: address(0), |
| 67 | + minHealthFactor: 0, |
| 68 | + maxHealthFactor: 0, |
| 69 | + includeZeroDebt: true, |
| 70 | + reverting: false |
| 71 | + }), |
| 72 | + 0 |
| 73 | + ); |
| 74 | + |
| 75 | + CreditAccountDataOld[] memory oldCAs = dc3.getCreditAccountsByCreditManager(creditManager, new PriceUpdate[](0)); |
| 76 | + |
| 77 | + // Compare total number of credit accounts |
| 78 | + assertEq(newCAs.length, oldCAs.length, "Credit accounts length mismatch"); |
| 79 | + |
| 80 | + // Compare credit accounts in order |
| 81 | + for (uint256 k; k < newCAs.length; k++) { |
| 82 | + assertEq(newCAs[k].creditAccount, oldCAs[k].addr, "Credit account address mismatch"); |
| 83 | + assertEq(newCAs[k].creditManager, oldCAs[k].creditManager, "Credit manager mismatch"); |
| 84 | + assertEq(newCAs[k].creditFacade, oldCAs[k].creditFacade, "Credit facade mismatch"); |
| 85 | + assertEq(newCAs[k].underlying, oldCAs[k].underlying, "Underlying mismatch"); |
| 86 | + assertEq(newCAs[k].owner, oldCAs[k].borrower, "Owner/borrower mismatch"); |
| 87 | + assertEq(newCAs[k].debt, oldCAs[k].debt, "Debt mismatch"); |
| 88 | + assertEq(newCAs[k].enabledTokensMask, oldCAs[k].enabledTokensMask, "Enabled tokens mask mismatch"); |
| 89 | + assertEq(newCAs[k].accruedInterest, oldCAs[k].accruedInterest, "Accrued interest mismatch"); |
| 90 | + assertEq(newCAs[k].accruedFees, oldCAs[k].accruedFees, "Accrued fees mismatch"); |
| 91 | + assertEq(newCAs[k].totalDebtUSD, oldCAs[k].totalDebtUSD, "Total debt USD mismatch"); |
| 92 | + assertEq(newCAs[k].totalValueUSD, oldCAs[k].totalValueUSD, "Total value USD mismatch"); |
| 93 | + assertEq(newCAs[k].twvUSD, oldCAs[k].twvUSD, "TWV USD mismatch"); |
| 94 | + assertEq(newCAs[k].healthFactor, oldCAs[k].healthFactor, "Health factor mismatch"); |
| 95 | + assertEq(newCAs[k].expirationDate, oldCAs[k].expirationDate, "Expiration date mismatch"); |
| 96 | + |
| 97 | + // Compare only tokens with non-zero balance from new implementation |
| 98 | + for (uint256 m; m < newCAs[k].tokens.length; m++) { |
| 99 | + TokenInfo memory newToken = newCAs[k].tokens[m]; |
| 100 | + |
| 101 | + // Find matching token in old implementation |
| 102 | + bool found = false; |
| 103 | + for (uint256 n; n < oldCAs[k].balances.length; n++) { |
| 104 | + if (oldCAs[k].balances[n].token == newToken.token) { |
| 105 | + TokenBalance memory oldToken = oldCAs[k].balances[n]; |
| 106 | + assertEq(newToken.balance, oldToken.balance, "Token balance mismatch"); |
| 107 | + assertEq(newToken.quota, oldToken.quota, "Token quota mismatch"); |
| 108 | + found = true; |
| 109 | + break; |
| 110 | + } |
| 111 | + } |
| 112 | + require(found, string.concat("Token not found in old implementation: ", vm.toString(newToken.token))); |
| 113 | + } |
| 114 | + |
| 115 | + // Verify that any non-zero balance tokens in old implementation are present in new implementation |
| 116 | + for (uint256 n; n < oldCAs[k].balances.length; n++) { |
| 117 | + TokenBalance memory oldToken = oldCAs[k].balances[n]; |
| 118 | + if (oldToken.balance > 1) { |
| 119 | + bool found = false; |
| 120 | + for (uint256 m; m < newCAs[k].tokens.length; m++) { |
| 121 | + if (newCAs[k].tokens[m].token == oldToken.token) { |
| 122 | + found = true; |
| 123 | + break; |
| 124 | + } |
| 125 | + } |
| 126 | + require( |
| 127 | + found, |
| 128 | + string.concat( |
| 129 | + "Token with non-zero balance not found in new implementation: ", vm.toString(oldToken.token) |
| 130 | + ) |
| 131 | + ); |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + function _refreshUpdatablePriceFeed(address priceFeed, bytes32 contractType) internal { |
| 138 | + if (contractType == "PRICE_FEED::REDSTONE") { |
| 139 | + uint256 initialTS = block.timestamp; |
| 140 | + |
| 141 | + bytes32 dataFeedId = RedstonePriceFeed(priceFeed).dataFeedId(); |
| 142 | + uint8 signersThreshold = RedstonePriceFeed(priceFeed).getUniqueSignersThreshold(); |
| 143 | + |
| 144 | + bytes memory payload = |
| 145 | + _getRedstonePayload(bytes32ToString((dataFeedId)), Strings.toString(signersThreshold)); |
| 146 | + |
| 147 | + if (payload.length == 0) return; |
| 148 | + |
| 149 | + (uint256 expectedPayloadTimestamp,) = abi.decode(payload, (uint256, bytes)); |
| 150 | + |
| 151 | + if (expectedPayloadTimestamp > block.timestamp) { |
| 152 | + vm.warp(expectedPayloadTimestamp); |
| 153 | + } |
| 154 | + |
| 155 | + try RedstonePriceFeed(priceFeed).updatePrice(payload) {} catch {} |
| 156 | + |
| 157 | + vm.warp(initialTS); |
| 158 | + } else { |
| 159 | + revert("Unknown updatable price feed type"); |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + function _getRedstonePayload(string memory dataFeedId, string memory signersThreshold) |
| 164 | + internal |
| 165 | + returns (bytes memory) |
| 166 | + { |
| 167 | + string[2] memory dataServiceIds = ["redstone-primary-prod", "redstone-arbitrum-prod"]; |
| 168 | + |
| 169 | + for (uint256 i = 0; i < dataServiceIds.length; ++i) { |
| 170 | + string[] memory args = new string[](6); |
| 171 | + args[0] = "npx"; |
| 172 | + args[1] = "ts-node"; |
| 173 | + args[2] = "./scripts/redstone.ts"; |
| 174 | + args[3] = dataServiceIds[i]; |
| 175 | + args[4] = dataFeedId; |
| 176 | + args[5] = signersThreshold; |
| 177 | + |
| 178 | + try vm.ffi(args) returns (bytes memory response) { |
| 179 | + return response; |
| 180 | + } catch {} |
| 181 | + } |
| 182 | + |
| 183 | + return ""; |
| 184 | + } |
| 185 | + |
| 186 | + function bytes32ToString(bytes32 _bytes32) public pure returns (string memory) { |
| 187 | + uint8 i = 0; |
| 188 | + while (i < 32 && _bytes32[i] != 0) { |
| 189 | + i++; |
| 190 | + } |
| 191 | + bytes memory bytesArray = new bytes(i); |
| 192 | + for (i = 0; i < 32 && _bytes32[i] != 0; i++) { |
| 193 | + bytesArray[i] = _bytes32[i]; |
| 194 | + } |
| 195 | + return string(bytesArray); |
| 196 | + } |
| 197 | +} |
0 commit comments