Skip to content

Commit 5798bbe

Browse files
committed
feat: equivalence tests for credit suites and credit accounts
1 parent 5be3341 commit 5798bbe

File tree

2 files changed

+395
-0
lines changed

2 files changed

+395
-0
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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 {MarketData} from "../../types/MarketData.sol";
11+
import {CreditManagerData} from "../../data/Types.sol";
12+
import {MarketFilter} from "../../types/Filters.sol";
13+
import "forge-std/console.sol";
14+
15+
contract CreditSuiteEquivalenceTest is Test {
16+
using Address for address;
17+
18+
DataCompressorV3 public dc3;
19+
MarketCompressor public mc;
20+
21+
function _findCreditManager(address cmAddr, CreditManagerData[] memory creditManagersOld)
22+
internal
23+
pure
24+
returns (uint256)
25+
{
26+
for (uint256 i; i < creditManagersOld.length; i++) {
27+
if (creditManagersOld[i].addr == cmAddr) {
28+
return i;
29+
}
30+
}
31+
revert(string.concat("No matching credit manager found for address: ", vm.toString(cmAddr)));
32+
}
33+
34+
function test_CSE_01_CreditSuite_equivalence() public {
35+
address marketCompressor = vm.envOr("MARKET_COMPRESSOR", address(0));
36+
address dataCompressor = vm.envOr("DATA_COMPRESSOR", address(0));
37+
38+
if (
39+
marketCompressor == address(0) || dataCompressor == address(0) || !marketCompressor.isContract()
40+
|| !dataCompressor.isContract()
41+
) {
42+
console.log(
43+
"MarketCompressor or DataCompressor not set, or not a contract. Skipping credit suite equivalence test."
44+
);
45+
return;
46+
}
47+
48+
mc = MarketCompressor(marketCompressor);
49+
dc3 = DataCompressorV3(dataCompressor);
50+
51+
MarketFilter memory filter =
52+
MarketFilter({configurators: new address[](0), pools: new address[](0), underlying: address(0)});
53+
54+
MarketData[] memory markets = mc.getMarkets(filter);
55+
CreditManagerData[] memory creditManagersOld = dc3.getCreditManagersV3List();
56+
57+
uint256 totalCreditManagers;
58+
for (uint256 i; i < markets.length; i++) {
59+
totalCreditManagers += markets[i].creditManagers.length;
60+
}
61+
62+
assertEq(totalCreditManagers, creditManagersOld.length, "Credit managers total count mismatch");
63+
64+
// For each credit manager in markets, find its match in creditManagersOld
65+
for (uint256 i; i < markets.length; i++) {
66+
for (uint256 j; j < markets[i].creditManagers.length; j++) {
67+
address cmAddr = markets[i].creditManagers[j].creditManager.baseParams.addr;
68+
uint256 cmIndex = _findCreditManager(cmAddr, creditManagersOld);
69+
70+
// Compare credit manager data
71+
assertEq(
72+
markets[i].creditManagers[j].creditManager.name,
73+
creditManagersOld[cmIndex].name,
74+
"Credit manager name mismatch"
75+
);
76+
assertEq(
77+
markets[i].creditManagers[j].creditManager.underlying,
78+
creditManagersOld[cmIndex].underlying,
79+
"Credit manager underlying mismatch"
80+
);
81+
assertEq(
82+
markets[i].creditManagers[j].creditManager.pool,
83+
creditManagersOld[cmIndex].pool,
84+
"Credit manager pool mismatch"
85+
);
86+
87+
// Compare credit facade data
88+
assertEq(
89+
markets[i].creditManagers[j].creditFacade.baseParams.addr,
90+
creditManagersOld[cmIndex].creditFacade,
91+
"Credit facade address mismatch"
92+
);
93+
// assertEq(
94+
// markets[i].creditManagers[j].creditFacade.baseParams.version,
95+
// creditManagersOld[cmIndex].creditFacadeVersion,
96+
// "Credit facade version mismatch"
97+
// );
98+
assertEq(
99+
markets[i].creditManagers[j].creditFacade.degenNFT,
100+
creditManagersOld[cmIndex].degenNFT,
101+
"Credit facade degenNFT mismatch"
102+
);
103+
assertEq(
104+
markets[i].creditManagers[j].creditFacade.forbiddenTokenMask,
105+
creditManagersOld[cmIndex].forbiddenTokenMask,
106+
"Credit facade forbidden token mask mismatch"
107+
);
108+
assertEq(
109+
markets[i].creditManagers[j].creditFacade.isPaused,
110+
creditManagersOld[cmIndex].isPaused,
111+
"Credit facade isPaused mismatch"
112+
);
113+
assertEq(
114+
markets[i].creditManagers[j].creditFacade.minDebt,
115+
creditManagersOld[cmIndex].minDebt,
116+
"Credit facade minDebt mismatch"
117+
);
118+
assertEq(
119+
markets[i].creditManagers[j].creditFacade.maxDebt,
120+
creditManagersOld[cmIndex].maxDebt,
121+
"Credit facade maxDebt mismatch"
122+
);
123+
124+
// Compare credit manager configuration
125+
assertEq(
126+
markets[i].creditManagers[j].creditManager.maxEnabledTokens,
127+
creditManagersOld[cmIndex].maxEnabledTokensLength,
128+
"Credit manager max enabled tokens mismatch"
129+
);
130+
assertEq(
131+
markets[i].creditManagers[j].creditManager.feeInterest,
132+
creditManagersOld[cmIndex].feeInterest,
133+
"Credit manager fee interest mismatch"
134+
);
135+
assertEq(
136+
markets[i].creditManagers[j].creditManager.feeLiquidation,
137+
creditManagersOld[cmIndex].feeLiquidation,
138+
"Credit manager fee liquidation mismatch"
139+
);
140+
assertEq(
141+
markets[i].creditManagers[j].creditManager.liquidationDiscount,
142+
creditManagersOld[cmIndex].liquidationDiscount,
143+
"Credit manager liquidation discount mismatch"
144+
);
145+
assertEq(
146+
markets[i].creditManagers[j].creditManager.feeLiquidationExpired,
147+
creditManagersOld[cmIndex].feeLiquidationExpired,
148+
"Credit manager fee liquidation expired mismatch"
149+
);
150+
assertEq(
151+
markets[i].creditManagers[j].creditManager.liquidationDiscountExpired,
152+
creditManagersOld[cmIndex].liquidationDiscountExpired,
153+
"Credit manager liquidation discount expired mismatch"
154+
);
155+
156+
// Compare collateral tokens
157+
assertEq(
158+
markets[i].creditManagers[j].creditManager.collateralTokens.length,
159+
creditManagersOld[cmIndex].collateralTokens.length,
160+
"Credit manager collateral tokens length mismatch"
161+
);
162+
163+
for (uint256 k; k < markets[i].creditManagers[j].creditManager.collateralTokens.length; k++) {
164+
assertEq(
165+
markets[i].creditManagers[j].creditManager.collateralTokens[k].token,
166+
creditManagersOld[cmIndex].collateralTokens[k],
167+
"Credit manager collateral token address mismatch"
168+
);
169+
assertEq(
170+
markets[i].creditManagers[j].creditManager.collateralTokens[k].liquidationThreshold,
171+
creditManagersOld[cmIndex].liquidationThresholds[k],
172+
"Credit manager collateral token liquidation threshold mismatch"
173+
);
174+
}
175+
176+
// Compare adapters
177+
assertEq(
178+
markets[i].creditManagers[j].adapters.length,
179+
creditManagersOld[cmIndex].adapters.length,
180+
"Credit manager adapters length mismatch"
181+
);
182+
183+
for (uint256 k; k < markets[i].creditManagers[j].adapters.length; k++) {
184+
assertEq(
185+
markets[i].creditManagers[j].adapters[k].baseParams.addr,
186+
creditManagersOld[cmIndex].adapters[k].adapter,
187+
"Credit manager adapter address mismatch"
188+
);
189+
assertEq(
190+
markets[i].creditManagers[j].adapters[k].targetContract,
191+
creditManagersOld[cmIndex].adapters[k].targetContract,
192+
"Credit manager adapter target contract mismatch"
193+
);
194+
}
195+
}
196+
}
197+
}
198+
}

0 commit comments

Comments
 (0)