@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