@gearbox-protocol/periphery-v3 1.7.0-next.87 → 1.7.0-next.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,185 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Gearbox Protocol. Generalized leverage for DeFi protocols
3
+ // (c) Gearbox Foundation, 2024.
4
+ pragma solidity ^0.8.17;
5
+
6
+ import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
7
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8
+ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
9
+ import {SafeERC20} from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol";
10
+
11
+ import {ICreditManagerV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditManagerV3.sol";
12
+ import {ICreditFacadeV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditFacadeV3.sol";
13
+ import {PriceUpdate} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeedStore.sol";
14
+ import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol";
15
+ import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v3/contracts/libraries/Constants.sol";
16
+
17
+ import {IMarketConfigurator} from "@gearbox-protocol/permissionless/contracts/interfaces/IMarketConfigurator.sol";
18
+ import {IContractsRegister} from "@gearbox-protocol/permissionless/contracts/interfaces/IContractsRegister.sol";
19
+
20
+ /**
21
+ * @title TreasuryLiquidator
22
+ * @notice This contract allows the treasury to liquidate credit accounts by providing the funds
23
+ * needed for liquidation and receiving the seized collateral. The treasury can set exchange rates
24
+ * for different token pairs, as well as approve liquidators to use this contract.
25
+ */
26
+ contract TreasuryLiquidator is SanityCheckTrait {
27
+ using SafeERC20 for IERC20;
28
+
29
+ /// @notice Contract type for identification
30
+ bytes32 public constant contractType = "TREASURY_LIQUIDATOR";
31
+
32
+ /// @notice Contract version
33
+ uint256 public constant version = 3_10;
34
+
35
+ /// @notice The treasury address that funds are taken from and returned to
36
+ address public immutable treasury;
37
+
38
+ /// @notice The market configurator address
39
+ address public immutable marketConfigurator;
40
+
41
+ /// @notice Mapping of approved liquidators who can use this contract
42
+ mapping(address => bool) public isLiquidator;
43
+
44
+ /// @notice Mapping of minimum exchange rates (assetIn => assetOut => rate)
45
+ /// Rate is in PERCENTAGE_FACTOR format (i.e. 10050 means 1.005 units of collateral per unit of underlying, regardless of decimals)
46
+ mapping(address => mapping(address => uint256)) public minExchangeRates;
47
+
48
+ // EVENTS
49
+ event PartiallyLiquidateFromTreasury(
50
+ address indexed creditFacade, address indexed creditAccount, address indexed liquidator
51
+ );
52
+ event SetLiquidatorStatus(address indexed liquidator, bool status);
53
+ event SetMinExchangeRate(address indexed assetIn, address indexed assetOut, uint256 rate);
54
+
55
+ // ERRORS
56
+ error CallerNotTreasuryException();
57
+ error CallerNotApprovedLiquidatorException();
58
+ error InsufficientTreasuryFundsException();
59
+ error UnsupportedTokenPairException();
60
+ error InvalidCreditSuiteException();
61
+
62
+ /// @notice Modifier to verify the sender is the treasury
63
+ modifier onlyTreasury() {
64
+ if (msg.sender != treasury) revert CallerNotTreasuryException();
65
+ _;
66
+ }
67
+
68
+ /// @notice Modifier to verify the sender is an approved liquidator
69
+ modifier onlyLiquidator() {
70
+ if (!isLiquidator[msg.sender]) revert CallerNotApprovedLiquidatorException();
71
+ _;
72
+ }
73
+
74
+ /// @notice Modifier to verify the credit facade is from the market configurator
75
+ modifier onlyCFFromMarketConfigurator(address creditFacade) {
76
+ address creditManager = ICreditFacadeV3(creditFacade).creditManager();
77
+ bool isValidCM = IContractsRegister(IMarketConfigurator(marketConfigurator).contractsRegister()).isCreditManager(
78
+ creditManager
79
+ );
80
+ if (!isValidCM || ICreditManagerV3(creditManager).creditFacade() != creditFacade) {
81
+ revert InvalidCreditSuiteException();
82
+ }
83
+ _;
84
+ }
85
+
86
+ /**
87
+ * @notice Constructor
88
+ * @param _treasury The address of the treasury
89
+ */
90
+ constructor(address _treasury, address _marketConfigurator)
91
+ nonZeroAddress(_treasury)
92
+ nonZeroAddress(_marketConfigurator)
93
+ {
94
+ treasury = _treasury;
95
+ marketConfigurator = _marketConfigurator;
96
+ }
97
+
98
+ /**
99
+ * @notice Set liquidator status
100
+ * @param liquidator The address to set status for
101
+ * @param status True to approve, false to revoke
102
+ */
103
+ function setLiquidatorStatus(address liquidator, bool status) external onlyTreasury nonZeroAddress(liquidator) {
104
+ if (isLiquidator[liquidator] == status) return;
105
+ isLiquidator[liquidator] = status;
106
+ emit SetLiquidatorStatus(liquidator, status);
107
+ }
108
+
109
+ /**
110
+ * @notice Set minimum exchange rate between two assets
111
+ * @param assetIn The asset being provided for liquidation
112
+ * @param assetOut The asset expected to be received from liquidation
113
+ * @param rate The minimum exchange rate (RATE_PRECISION format)
114
+ */
115
+ function setMinExchangeRate(address assetIn, address assetOut, uint256 rate)
116
+ external
117
+ onlyTreasury
118
+ nonZeroAddress(assetIn)
119
+ nonZeroAddress(assetOut)
120
+ {
121
+ if (minExchangeRates[assetIn][assetOut] == rate) return;
122
+
123
+ minExchangeRates[assetIn][assetOut] = rate;
124
+ emit SetMinExchangeRate(assetIn, assetOut, rate);
125
+ }
126
+
127
+ /**
128
+ * @notice Partially liquidate a credit account using funds from the treasury
129
+ * @param creditFacade The credit facade contract
130
+ * @param creditAccount The credit account to partially liquidate
131
+ * @param token The collateral token to seize
132
+ * @param repaidAmount The amount of underlying to repay
133
+ * @param priceUpdates Optional price updates to apply before liquidation
134
+ */
135
+ function partiallyLiquidateFromTreasury(
136
+ address creditFacade,
137
+ address creditAccount,
138
+ address token,
139
+ uint256 repaidAmount,
140
+ PriceUpdate[] calldata priceUpdates,
141
+ address wrappedUnderlying
142
+ ) external onlyLiquidator onlyCFFromMarketConfigurator(creditFacade) {
143
+ address underlying = ICreditFacadeV3(creditFacade).underlying();
144
+
145
+ uint256 minSeizedAmount = _getMinSeizedAmount(underlying, token, repaidAmount);
146
+
147
+ _transferUnderlying(underlying, wrappedUnderlying, repaidAmount);
148
+ {
149
+ address creditManager = ICreditFacadeV3(creditFacade).creditManager();
150
+ IERC20(underlying).forceApprove(creditManager, repaidAmount);
151
+ }
152
+
153
+ ICreditFacadeV3(creditFacade).partiallyLiquidateCreditAccount(
154
+ creditAccount, token, repaidAmount, minSeizedAmount, treasury, priceUpdates
155
+ );
156
+
157
+ emit PartiallyLiquidateFromTreasury(creditFacade, creditAccount, msg.sender);
158
+ }
159
+
160
+ function _getMinSeizedAmount(address underlying, address token, uint256 repaidAmount)
161
+ internal
162
+ view
163
+ returns (uint256)
164
+ {
165
+ uint256 requiredRate = minExchangeRates[underlying][token];
166
+ if (requiredRate == 0) revert UnsupportedTokenPairException();
167
+
168
+ uint256 scaleUnderlying = 10 ** IERC20Metadata(underlying).decimals();
169
+ uint256 scaleToken = 10 ** IERC20Metadata(token).decimals();
170
+
171
+ return repaidAmount * requiredRate * scaleToken / (PERCENTAGE_FACTOR * scaleUnderlying);
172
+ }
173
+
174
+ function _transferUnderlying(address underlying, address wrappedUnderlying, uint256 amount) internal {
175
+ if (wrappedUnderlying != address(0)) {
176
+ uint256 wrappedAssets = IERC4626(wrappedUnderlying).maxWithdraw(treasury);
177
+ if (wrappedAssets < amount) revert InsufficientTreasuryFundsException();
178
+ IERC4626(wrappedUnderlying).withdraw(amount, address(this), treasury);
179
+ } else {
180
+ uint256 balance = IERC20(underlying).balanceOf(treasury);
181
+ if (balance < amount) revert InsufficientTreasuryFundsException();
182
+ IERC20(underlying).safeTransferFrom(treasury, address(this), amount);
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,23 @@
1
+ // SPDX-License-Identifier: GPL-2.0-or-later
2
+ // Gearbox Protocol. Generalized leverage for DeFi protocols
3
+ // (c) Gearbox Foundation, 2025.
4
+ pragma solidity ^0.8.23;
5
+
6
+ import {ITimeLock} from "@gearbox-protocol/permissionless/contracts/interfaces/ITimeLock.sol";
7
+ import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol";
8
+
9
+ contract BatchesChain is IVersion {
10
+ /// @notice Contract type
11
+ bytes32 public constant override contractType = "BATCHES_CHAIN";
12
+
13
+ /// @notice Contract version
14
+ uint256 public constant override version = 3_10;
15
+
16
+ /// @notice Check if tx is queued in TimeLock
17
+ /// @dev Reverts if tx was not executed
18
+ function revertIfQueued(bytes32 txHash) external view {
19
+ if (ITimeLock(msg.sender).queuedTransactions(txHash)) {
20
+ revert("BatchesChain: transaction isn't executed yet");
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,514 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ // Gearbox Protocol. Generalized leverage for DeFi protocols
3
+ // (c) Gearbox Foundation, 2024.
4
+ pragma solidity ^0.8.17;
5
+
6
+ import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
7
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8
+ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
9
+ import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
10
+ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
11
+
12
+ import {ICreditAccountV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditAccountV3.sol";
13
+ import {
14
+ CollateralDebtData,
15
+ CollateralCalcTask,
16
+ ICreditManagerV3,
17
+ ICreditManagerV3Events,
18
+ ManageDebtAction
19
+ } from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditManagerV3.sol";
20
+ import {IPriceFeedStore, PriceUpdate} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeedStore.sol";
21
+ import {
22
+ ICreditFacadeV3,
23
+ ICreditFacadeV3Multicall,
24
+ MultiCall
25
+ } from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditFacadeV3.sol";
26
+ import {MultiCallBuilder} from "@gearbox-protocol/core-v3/contracts/test/lib/MultiCallBuilder.sol";
27
+ import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v3/contracts/libraries/Constants.sol";
28
+
29
+ // TESTS
30
+ import "@gearbox-protocol/core-v3/contracts/test/lib/constants.sol";
31
+ import {IntegrationTestHelper} from "@gearbox-protocol/core-v3/contracts/test/helpers/IntegrationTestHelper.sol";
32
+
33
+ // EXCEPTIONS
34
+ import "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol";
35
+
36
+ // MOCKS
37
+ import {AdapterMock} from "@gearbox-protocol/core-v3/contracts/test/mocks/core/AdapterMock.sol";
38
+ import {PriceFeedMock} from "@gearbox-protocol/core-v3/contracts/test/mocks/oracles/PriceFeedMock.sol";
39
+
40
+ import {IMarketConfigurator} from "@gearbox-protocol/permissionless/contracts/interfaces/IMarketConfigurator.sol";
41
+ import {IContractsRegister} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IContractsRegister.sol";
42
+
43
+ import {TreasuryLiquidator} from "../emergency/TreasuryLiquidator.sol";
44
+
45
+ contract ERC4626Mock is ERC4626 {
46
+ constructor(address asset, string memory name, string memory symbol) ERC4626(IERC20(asset)) ERC20(name, symbol) {}
47
+ }
48
+
49
+ contract TreasuryLiquidatorIntegrationTest is IntegrationTestHelper {
50
+ TreasuryLiquidator treasuryLiquidator;
51
+ address treasury;
52
+ address liquidator;
53
+ address marketConfigurator;
54
+ ERC4626Mock wrappedUnderlying;
55
+
56
+ // Events from TreasuryLiquidator
57
+ event PartiallyLiquidateFromTreasury(
58
+ address indexed creditFacade, address indexed creditAccount, address indexed liquidator
59
+ );
60
+ event SetLiquidatorStatus(address indexed liquidator, bool status);
61
+ event SetMinExchangeRate(address indexed assetIn, address indexed assetOut, uint256 rate);
62
+
63
+ function _setupTreasuryLiquidator() internal {
64
+ treasury = makeAddr("TREASURY");
65
+ liquidator = makeAddr("LIQUIDATOR");
66
+ marketConfigurator = makeAddr("MARKET_CONFIGURATOR");
67
+
68
+ // Deploy TreasuryLiquidator
69
+ treasuryLiquidator = new TreasuryLiquidator(treasury, marketConfigurator);
70
+
71
+ // Create wrapped underlying mock
72
+ wrappedUnderlying = new ERC4626Mock(underlying, "Wrapped DAI", "wDAI");
73
+
74
+ // Setup initial balances
75
+ tokenTestSuite.mint(underlying, treasury, 1000000e18);
76
+
77
+ // Deposit some underlying into wrapped version for treasury
78
+ vm.startPrank(treasury);
79
+ IERC20(underlying).approve(address(wrappedUnderlying), 500000e18);
80
+ wrappedUnderlying.mint(500000e18, treasury);
81
+ vm.stopPrank();
82
+ }
83
+
84
+ function _makeCreditAccount() internal returns (address) {
85
+ uint256 debtAmount = DAI_ACCOUNT_AMOUNT;
86
+ uint256 bufferedDebtAmount = 11 * debtAmount / 10;
87
+ uint256 collateralAmount = priceOracle.convert(
88
+ bufferedDebtAmount * PERCENTAGE_FACTOR / creditManager.liquidationThresholds(weth), underlying, weth
89
+ );
90
+
91
+ tokenTestSuite.mint(weth, USER, collateralAmount);
92
+ tokenTestSuite.approve(weth, USER, address(creditManager));
93
+
94
+ vm.prank(USER);
95
+ return creditFacade.openCreditAccount(
96
+ USER,
97
+ MultiCallBuilder.build(
98
+ MultiCall({
99
+ target: address(creditFacade),
100
+ callData: abi.encodeCall(ICreditFacadeV3Multicall.increaseDebt, (debtAmount))
101
+ }),
102
+ MultiCall({
103
+ target: address(creditFacade),
104
+ callData: abi.encodeCall(ICreditFacadeV3Multicall.withdrawCollateral, (underlying, debtAmount, USER))
105
+ }),
106
+ MultiCall({
107
+ target: address(creditFacade),
108
+ callData: abi.encodeCall(ICreditFacadeV3Multicall.addCollateral, (weth, collateralAmount))
109
+ }),
110
+ MultiCall({
111
+ target: address(creditFacade),
112
+ callData: abi.encodeCall(
113
+ ICreditFacadeV3Multicall.updateQuota, (weth, int96(uint96(bufferedDebtAmount)), 0)
114
+ )
115
+ })
116
+ ),
117
+ 0
118
+ );
119
+ }
120
+
121
+ function _setupContractsRegister(bool isValidCM) internal {
122
+ // Mock the market configurator to return valid contracts register
123
+ vm.mockCall(
124
+ marketConfigurator,
125
+ abi.encodeWithSelector(IMarketConfigurator.contractsRegister.selector),
126
+ abi.encode(address(cr))
127
+ );
128
+
129
+ // Mock contracts register to validate credit manager
130
+ vm.mockCall(
131
+ address(cr),
132
+ abi.encodeWithSelector(IContractsRegister.isCreditManager.selector, address(creditManager)),
133
+ abi.encode(isValidCM)
134
+ );
135
+ }
136
+
137
+ function _purgeWeth(address creditAccount) internal {
138
+ CollateralDebtData memory cdd =
139
+ creditManager.calcDebtAndCollateral(creditAccount, CollateralCalcTask.DEBT_COLLATERAL);
140
+
141
+ uint256 debtEquivalent = priceOracle.convertFromUSD(cdd.totalDebtUSD, weth) * PERCENTAGE_FACTOR
142
+ / creditManager.liquidationThresholds(weth);
143
+ uint256 tokenBalance = tokenTestSuite.balanceOf(weth, creditAccount);
144
+
145
+ vm.prank(creditAccount);
146
+ IERC20(weth).transfer(address(1), tokenBalance - (debtEquivalent * 9999 / PERCENTAGE_FACTOR));
147
+ }
148
+
149
+ /// @dev I:[TL-1]: Constructor sets correct values
150
+ function test_I_TL_01_constructor_sets_correct_values() public creditTest {
151
+ _setupTreasuryLiquidator();
152
+ assertEq(treasuryLiquidator.treasury(), treasury, "Treasury address mismatch");
153
+ assertEq(treasuryLiquidator.marketConfigurator(), marketConfigurator, "Market configurator mismatch");
154
+ assertEq(treasuryLiquidator.contractType(), "TREASURY_LIQUIDATOR", "Contract type mismatch");
155
+ assertEq(treasuryLiquidator.version(), 3_10, "Version mismatch");
156
+ }
157
+
158
+ /// @dev I:[TL-2]: Constructor reverts on zero addresses
159
+ function test_I_TL_02_constructor_reverts_on_zero_addresses() public {
160
+ vm.expectRevert(ZeroAddressException.selector);
161
+ new TreasuryLiquidator(address(0), makeAddr("MARKET_CONFIGURATOR"));
162
+
163
+ vm.expectRevert(ZeroAddressException.selector);
164
+ new TreasuryLiquidator(makeAddr("TREASURY"), address(0));
165
+ }
166
+
167
+ /// @dev I:[TL-3]: setLiquidatorStatus works correctly for treasury
168
+ function test_I_TL_03_setLiquidatorStatus_works_correctly_for_treasury() public creditTest {
169
+ _setupTreasuryLiquidator();
170
+ assertFalse(treasuryLiquidator.isLiquidator(liquidator), "Liquidator should not be approved initially");
171
+
172
+ vm.expectEmit(true, false, false, true);
173
+ emit SetLiquidatorStatus(liquidator, true);
174
+
175
+ vm.prank(treasury);
176
+ treasuryLiquidator.setLiquidatorStatus(liquidator, true);
177
+
178
+ assertTrue(treasuryLiquidator.isLiquidator(liquidator), "Liquidator should be approved");
179
+
180
+ // Test revoking status
181
+ vm.expectEmit(true, false, false, true);
182
+ emit SetLiquidatorStatus(liquidator, false);
183
+
184
+ vm.prank(treasury);
185
+ treasuryLiquidator.setLiquidatorStatus(liquidator, false);
186
+
187
+ assertFalse(treasuryLiquidator.isLiquidator(liquidator), "Liquidator should be revoked");
188
+ }
189
+
190
+ /// @dev I:[TL-4]: setLiquidatorStatus reverts for non-treasury
191
+ function test_I_TL_04_setLiquidatorStatus_reverts_for_non_treasury() public creditTest {
192
+ _setupTreasuryLiquidator();
193
+ vm.expectRevert(TreasuryLiquidator.CallerNotTreasuryException.selector);
194
+ vm.prank(USER);
195
+ treasuryLiquidator.setLiquidatorStatus(liquidator, true);
196
+ }
197
+
198
+ /// @dev I:[TL-5]: setLiquidatorStatus reverts on zero address
199
+ function test_I_TL_05_setLiquidatorStatus_reverts_on_zero_address() public creditTest {
200
+ _setupTreasuryLiquidator();
201
+ vm.expectRevert(ZeroAddressException.selector);
202
+ vm.prank(treasury);
203
+ treasuryLiquidator.setLiquidatorStatus(address(0), true);
204
+ }
205
+
206
+ /// @dev I:[TL-6]: setLiquidatorStatus does nothing if status unchanged
207
+ function test_I_TL_06_setLiquidatorStatus_does_nothing_if_status_unchanged() public creditTest {
208
+ _setupTreasuryLiquidator();
209
+ // Set liquidator status to true
210
+ vm.prank(treasury);
211
+ treasuryLiquidator.setLiquidatorStatus(liquidator, true);
212
+
213
+ // Try to set the same status again - should not emit event
214
+ vm.recordLogs();
215
+ vm.prank(treasury);
216
+ treasuryLiquidator.setLiquidatorStatus(liquidator, true);
217
+
218
+ // Check no events were emitted
219
+ assertEq(vm.getRecordedLogs().length, 0, "No events should be emitted for unchanged status");
220
+ }
221
+
222
+ /// @dev I:[TL-7]: setMinExchangeRate works correctly for treasury
223
+ function test_I_TL_07_setMinExchangeRate_works_correctly_for_treasury() public creditTest {
224
+ _setupTreasuryLiquidator();
225
+ uint256 rate = 10050; // 1.005 in PERCENTAGE_FACTOR format
226
+
227
+ assertEq(treasuryLiquidator.minExchangeRates(underlying, weth), 0, "Rate should be 0 initially");
228
+
229
+ vm.expectEmit(true, true, false, true);
230
+ emit SetMinExchangeRate(underlying, weth, rate);
231
+
232
+ vm.prank(treasury);
233
+ treasuryLiquidator.setMinExchangeRate(underlying, weth, rate);
234
+
235
+ assertEq(treasuryLiquidator.minExchangeRates(underlying, weth), rate, "Rate should be set correctly");
236
+ }
237
+
238
+ /// @dev I:[TL-8]: setMinExchangeRate reverts for non-treasury
239
+ function test_I_TL_08_setMinExchangeRate_reverts_for_non_treasury() public creditTest {
240
+ _setupTreasuryLiquidator();
241
+ vm.expectRevert(TreasuryLiquidator.CallerNotTreasuryException.selector);
242
+ vm.prank(USER);
243
+ treasuryLiquidator.setMinExchangeRate(underlying, weth, 10050);
244
+ }
245
+
246
+ /// @dev I:[TL-9]: setMinExchangeRate reverts on zero addresses
247
+ function test_I_TL_09_setMinExchangeRate_reverts_on_zero_addresses() public creditTest {
248
+ _setupTreasuryLiquidator();
249
+ vm.expectRevert(ZeroAddressException.selector);
250
+ vm.prank(treasury);
251
+ treasuryLiquidator.setMinExchangeRate(address(0), weth, 10050);
252
+
253
+ vm.expectRevert(ZeroAddressException.selector);
254
+ vm.prank(treasury);
255
+ treasuryLiquidator.setMinExchangeRate(underlying, address(0), 10050);
256
+ }
257
+
258
+ /// @dev I:[TL-10]: setMinExchangeRate does nothing if rate unchanged
259
+ function test_I_TL_10_setMinExchangeRate_does_nothing_if_rate_unchanged() public creditTest {
260
+ _setupTreasuryLiquidator();
261
+ uint256 rate = 10050;
262
+
263
+ // Set rate initially
264
+ vm.prank(treasury);
265
+ treasuryLiquidator.setMinExchangeRate(underlying, weth, rate);
266
+
267
+ // Try to set the same rate again - should not emit event
268
+ vm.recordLogs();
269
+ vm.prank(treasury);
270
+ treasuryLiquidator.setMinExchangeRate(underlying, weth, rate);
271
+
272
+ // Check no events were emitted
273
+ assertEq(vm.getRecordedLogs().length, 0, "No events should be emitted for unchanged rate");
274
+ }
275
+
276
+ /// @dev I:[TL-11]: partiallyLiquidateFromTreasury reverts for non-liquidator
277
+ function test_I_TL_11_partiallyLiquidateFromTreasury_reverts_for_non_liquidator() public creditTest {
278
+ _setupTreasuryLiquidator();
279
+ address creditAccount = _makeCreditAccount();
280
+ _setupContractsRegister(true);
281
+
282
+ vm.expectRevert(TreasuryLiquidator.CallerNotApprovedLiquidatorException.selector);
283
+ vm.prank(USER);
284
+ treasuryLiquidator.partiallyLiquidateFromTreasury(
285
+ address(creditFacade), creditAccount, weth, 1000e18, new PriceUpdate[](0), address(0)
286
+ );
287
+ }
288
+
289
+ /// @dev I:[TL-12]: partiallyLiquidateFromTreasury reverts for invalid credit suite
290
+ function test_I_TL_12_partiallyLiquidateFromTreasury_reverts_for_invalid_credit_suite() public creditTest {
291
+ _setupTreasuryLiquidator();
292
+ _setupContractsRegister(false);
293
+ address creditAccount = _makeCreditAccount();
294
+
295
+ // Setup liquidator
296
+ vm.prank(treasury);
297
+ treasuryLiquidator.setLiquidatorStatus(liquidator, true);
298
+
299
+ // Don't setup valid credit suite - should revert
300
+ vm.expectRevert(TreasuryLiquidator.InvalidCreditSuiteException.selector);
301
+ vm.prank(liquidator);
302
+ treasuryLiquidator.partiallyLiquidateFromTreasury(
303
+ address(creditFacade), creditAccount, weth, 1000e18, new PriceUpdate[](0), address(0)
304
+ );
305
+ }
306
+
307
+ /// @dev I:[TL-13]: partiallyLiquidateFromTreasury reverts for unsupported token pair
308
+ function test_I_TL_13_partiallyLiquidateFromTreasury_reverts_for_unsupported_token_pair() public creditTest {
309
+ _setupTreasuryLiquidator();
310
+ address creditAccount = _makeCreditAccount();
311
+ _setupContractsRegister(true);
312
+
313
+ // Setup liquidator
314
+ vm.prank(treasury);
315
+ treasuryLiquidator.setLiquidatorStatus(liquidator, true);
316
+
317
+ // Don't set exchange rate - should revert
318
+ vm.expectRevert(TreasuryLiquidator.UnsupportedTokenPairException.selector);
319
+ vm.prank(liquidator);
320
+ treasuryLiquidator.partiallyLiquidateFromTreasury(
321
+ address(creditFacade), creditAccount, weth, 1000e18, new PriceUpdate[](0), address(0)
322
+ );
323
+ }
324
+
325
+ /// @dev I:[TL-14]: partiallyLiquidateFromTreasury reverts for insufficient treasury funds
326
+ function test_I_TL_14_partiallyLiquidateFromTreasury_reverts_for_insufficient_treasury_funds() public creditTest {
327
+ _setupTreasuryLiquidator();
328
+ address creditAccount = _makeCreditAccount();
329
+ _setupContractsRegister(true);
330
+
331
+ // Setup liquidator and exchange rate
332
+ vm.prank(treasury);
333
+ treasuryLiquidator.setLiquidatorStatus(liquidator, true);
334
+
335
+ vm.prank(treasury);
336
+ treasuryLiquidator.setMinExchangeRate(underlying, weth, 10050);
337
+
338
+ // Remove treasury funds
339
+ vm.startPrank(treasury);
340
+ IERC20(underlying).transfer(USER, IERC20(underlying).balanceOf(treasury));
341
+ vm.stopPrank();
342
+
343
+ vm.expectRevert(TreasuryLiquidator.InsufficientTreasuryFundsException.selector);
344
+ vm.prank(liquidator);
345
+ treasuryLiquidator.partiallyLiquidateFromTreasury(
346
+ address(creditFacade), creditAccount, weth, 1000e18, new PriceUpdate[](0), address(0)
347
+ );
348
+ }
349
+
350
+ /// @dev I:[TL-15]: partiallyLiquidateFromTreasury reverts for insufficient wrapped treasury funds
351
+ function test_I_TL_15_partiallyLiquidateFromTreasury_reverts_for_insufficient_wrapped_treasury_funds()
352
+ public
353
+ creditTest
354
+ {
355
+ _setupTreasuryLiquidator();
356
+ address creditAccount = _makeCreditAccount();
357
+ _setupContractsRegister(true);
358
+
359
+ vm.prank(treasury);
360
+ treasuryLiquidator.setLiquidatorStatus(liquidator, true);
361
+
362
+ vm.prank(treasury);
363
+ treasuryLiquidator.setMinExchangeRate(underlying, weth, 10050);
364
+
365
+ vm.startPrank(treasury);
366
+ wrappedUnderlying.redeem(wrappedUnderlying.balanceOf(treasury), treasury, treasury);
367
+ vm.stopPrank();
368
+
369
+ vm.expectRevert(TreasuryLiquidator.InsufficientTreasuryFundsException.selector);
370
+ vm.prank(liquidator);
371
+ treasuryLiquidator.partiallyLiquidateFromTreasury(
372
+ address(creditFacade), creditAccount, weth, 1000e18, new PriceUpdate[](0), address(wrappedUnderlying)
373
+ );
374
+ }
375
+
376
+ /// @dev I:[TL-16]: partiallyLiquidateFromTreasury works correctly with direct underlying
377
+ function test_I_TL_16_partiallyLiquidateFromTreasury_works_correctly_with_direct_underlying() public creditTest {
378
+ _setupTreasuryLiquidator();
379
+ address creditAccount = _makeCreditAccount();
380
+ _purgeWeth(creditAccount);
381
+ _setupContractsRegister(true);
382
+
383
+ vm.roll(block.number + 1);
384
+
385
+ // Setup liquidator and exchange rate
386
+ vm.prank(treasury);
387
+ treasuryLiquidator.setLiquidatorStatus(liquidator, true);
388
+
389
+ uint256 rate = 9;
390
+ vm.prank(treasury);
391
+ treasuryLiquidator.setMinExchangeRate(underlying, weth, rate);
392
+
393
+ uint256 repaidAmount = 1000e18;
394
+ uint256 treasuryBalanceBefore = IERC20(underlying).balanceOf(treasury);
395
+ uint256 treasuryWethBalanceBefore = IERC20(weth).balanceOf(treasury);
396
+
397
+ // Approve treasury to spend from treasury (for transferFrom)
398
+ vm.prank(treasury);
399
+ IERC20(underlying).approve(address(treasuryLiquidator), repaidAmount);
400
+
401
+ vm.expectEmit(true, true, true, false);
402
+ emit PartiallyLiquidateFromTreasury(address(creditFacade), creditAccount, liquidator);
403
+
404
+ vm.prank(liquidator);
405
+ treasuryLiquidator.partiallyLiquidateFromTreasury(
406
+ address(creditFacade), creditAccount, weth, repaidAmount, new PriceUpdate[](0), address(0)
407
+ );
408
+
409
+ assertEq(
410
+ IERC20(underlying).balanceOf(treasury),
411
+ treasuryBalanceBefore - repaidAmount,
412
+ "Treasury underlying balance should decrease"
413
+ );
414
+
415
+ assertGt(
416
+ IERC20(weth).balanceOf(treasury),
417
+ treasuryWethBalanceBefore + 9e17,
418
+ "Treasury should receive at least 9e17 weth"
419
+ );
420
+ }
421
+
422
+ /// @dev I:[TL-17]: partiallyLiquidateFromTreasury works correctly with wrapped underlying
423
+ function test_I_TL_17_partiallyLiquidateFromTreasury_works_correctly_with_wrapped_underlying() public creditTest {
424
+ _setupTreasuryLiquidator();
425
+ address creditAccount = _makeCreditAccount();
426
+ _purgeWeth(creditAccount);
427
+ _setupContractsRegister(true);
428
+
429
+ vm.roll(block.number + 1);
430
+
431
+ // Setup liquidator and exchange rate
432
+ vm.prank(treasury);
433
+ treasuryLiquidator.setLiquidatorStatus(liquidator, true);
434
+
435
+ uint256 rate = 9;
436
+ vm.prank(treasury);
437
+ treasuryLiquidator.setMinExchangeRate(underlying, weth, rate);
438
+
439
+ uint256 repaidAmount = 1000e18;
440
+ uint256 treasuryWrappedBalanceBefore = wrappedUnderlying.maxWithdraw(treasury);
441
+ uint256 treasuryWethBalanceBefore = IERC20(weth).balanceOf(treasury);
442
+
443
+ vm.prank(treasury);
444
+ IERC20(wrappedUnderlying).approve(address(treasuryLiquidator), treasuryWrappedBalanceBefore);
445
+
446
+ vm.expectEmit(true, true, true, false);
447
+ emit PartiallyLiquidateFromTreasury(address(creditFacade), creditAccount, liquidator);
448
+
449
+ vm.prank(liquidator);
450
+ treasuryLiquidator.partiallyLiquidateFromTreasury(
451
+ address(creditFacade), creditAccount, weth, repaidAmount, new PriceUpdate[](0), address(wrappedUnderlying)
452
+ );
453
+
454
+ assertEq(
455
+ wrappedUnderlying.maxWithdraw(treasury),
456
+ treasuryWrappedBalanceBefore - repaidAmount,
457
+ "Treasury wrapped balance is incorrect"
458
+ );
459
+
460
+ assertGt(
461
+ IERC20(weth).balanceOf(treasury),
462
+ treasuryWethBalanceBefore + 9e17,
463
+ "Treasury should receive at least 9e17 weth"
464
+ );
465
+ }
466
+
467
+ /// @dev I:[TL-18]: partiallyLiquidateFromTreasury calculates minimum seized amount correctly
468
+ function test_I_TL_18_partiallyLiquidateFromTreasury_calculates_minimum_seized_amount_correctly()
469
+ public
470
+ creditTest
471
+ {
472
+ _setupTreasuryLiquidator();
473
+ address creditAccount = _makeCreditAccount();
474
+ _purgeWeth(creditAccount);
475
+ _setupContractsRegister(true);
476
+
477
+ vm.roll(block.number + 1);
478
+
479
+ vm.prank(treasury);
480
+ treasuryLiquidator.setLiquidatorStatus(liquidator, true);
481
+
482
+ uint256 rate = 9;
483
+ vm.prank(treasury);
484
+ treasuryLiquidator.setMinExchangeRate(underlying, weth, rate);
485
+
486
+ uint256 repaidAmount = 1000e18;
487
+
488
+ uint256 scaleUnderlying = 10 ** IERC20Metadata(underlying).decimals(); // 18
489
+ uint256 scaleWeth = 10 ** IERC20Metadata(weth).decimals(); // 18
490
+ uint256 expectedMinSeized = repaidAmount * rate * scaleWeth / (PERCENTAGE_FACTOR * scaleUnderlying);
491
+
492
+ assertEq(expectedMinSeized, 9e17, "Expected minimum seized amount calculation");
493
+ vm.prank(treasury);
494
+ IERC20(underlying).approve(address(treasuryLiquidator), repaidAmount);
495
+
496
+ vm.expectCall(
497
+ address(creditFacade),
498
+ abi.encodeWithSelector(
499
+ ICreditFacadeV3.partiallyLiquidateCreditAccount.selector,
500
+ creditAccount,
501
+ weth,
502
+ repaidAmount,
503
+ expectedMinSeized,
504
+ treasury,
505
+ new PriceUpdate[](0)
506
+ )
507
+ );
508
+
509
+ vm.prank(liquidator);
510
+ treasuryLiquidator.partiallyLiquidateFromTreasury(
511
+ address(creditFacade), creditAccount, weth, repaidAmount, new PriceUpdate[](0), address(0)
512
+ );
513
+ }
514
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gearbox-protocol/periphery-v3",
3
- "version": "1.7.0-next.87",
3
+ "version": "1.7.0-next.89",
4
4
  "main": "index.js",
5
5
  "repository": "git@github.com:Gearbox-protocol/periphery-v3.git",
6
6
  "author": "Mikael <26343374+0xmikko@users.noreply.github.com>",