This repository has been archived by the owner on Mar 3, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
LiquidationRow.sol
326 lines (272 loc) · 14.2 KB
/
LiquidationRow.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity 0.8.17;
import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import { ReentrancyGuard } from "openzeppelin-contracts/security/ReentrancyGuard.sol";
import { EnumerableSet } from "openzeppelin-contracts/utils/structs/EnumerableSet.sol";
import { Address } from "openzeppelin-contracts/utils/Address.sol";
import { ISystemRegistry } from "src/interfaces/ISystemRegistry.sol";
import { IAsyncSwapper, SwapParams } from "src/interfaces/liquidation/IAsyncSwapper.sol";
import { ILiquidationRow } from "src/interfaces/liquidation/ILiquidationRow.sol";
import { IDestinationVault } from "src/interfaces/vault/IDestinationVault.sol";
import { IMainRewarder } from "src/interfaces/rewarders/IMainRewarder.sol";
import { IDestinationVaultRegistry } from "src/interfaces/vault/IDestinationVaultRegistry.sol";
import { SystemComponent } from "src/SystemComponent.sol";
import { LibAdapter } from "src/libs/LibAdapter.sol";
import { SecurityBase } from "src/security/SecurityBase.sol";
import { Roles } from "src/libs/Roles.sol";
import { Errors } from "src/utils/Errors.sol";
contract LiquidationRow is ILiquidationRow, ReentrancyGuard, SystemComponent, SecurityBase {
using Address for address;
using SafeERC20 for IERC20;
using EnumerableSet for EnumerableSet.AddressSet;
/// @notice An instance of the DestinationVaultRegistry contract.
IDestinationVaultRegistry internal immutable destinationVaultRegistry;
EnumerableSet.AddressSet private rewardTokens;
/// @notice Whitelisted swapper for liquidating vaults for token
EnumerableSet.AddressSet private whitelistedSwappers;
/// @notice Mapping to store the balance amount for each vault for each token
mapping(address => mapping(address => uint256)) private balances;
/// @notice Mapping to store the total balance for each token
mapping(address => uint256) private totalTokenBalances;
/// @notice Mapping to store the list of vaults for each token
mapping(address => EnumerableSet.AddressSet) private tokenVaults;
/// @notice Fee in basis points (bps). 1 bps is 0.01%
uint256 public feeBps = 0;
/// @notice Address to receive the fees
address public feeReceiver;
uint256 public constant MAX_PCT = 10_000;
constructor(ISystemRegistry _systemRegistry)
SystemComponent(_systemRegistry)
SecurityBase(address(_systemRegistry.accessController()))
{
destinationVaultRegistry = _systemRegistry.destinationVaultRegistry();
// System registry must be properly initialized first
Errors.verifyNotZero(address(destinationVaultRegistry), "destinationVaultRegistry");
}
/// @notice Restricts access to whitelisted swappers
modifier onlyWhitelistedSwapper(address swapper) {
if (!whitelistedSwappers.contains(swapper)) {
revert Errors.AccessDenied();
}
_;
}
/// @inheritdoc ILiquidationRow
function addToWhitelist(address swapper) external hasRole(Roles.LIQUIDATOR_ROLE) {
Errors.verifyNotZero(swapper, "swapper");
if (!whitelistedSwappers.add(swapper)) revert Errors.ItemExists();
emit SwapperAdded(swapper);
}
/// @inheritdoc ILiquidationRow
function removeFromWhitelist(address swapper) external hasRole(Roles.LIQUIDATOR_ROLE) {
if (!whitelistedSwappers.remove(swapper)) revert Errors.ItemNotFound();
emit SwapperRemoved(swapper);
}
/// @inheritdoc ILiquidationRow
function isWhitelisted(address swapper) external view returns (bool) {
return whitelistedSwappers.contains(swapper);
}
/// @inheritdoc ILiquidationRow
function setFeeAndReceiver(address _feeReceiver, uint256 _feeBps) external hasRole(Roles.LIQUIDATOR_ROLE) {
// feeBps should be less than or equal to MAX_PCT (100%) to prevent overflows
if (_feeBps > MAX_PCT) revert FeeTooHigh();
feeBps = _feeBps;
// slither-disable-next-line missing-zero-check
feeReceiver = _feeReceiver;
}
function calculateFee(uint256 amount) public view returns (uint256) {
return (amount * feeBps) / MAX_PCT;
}
/// @inheritdoc ILiquidationRow
function claimsVaultRewards(IDestinationVault[] memory vaults)
external
nonReentrant
hasRole(Roles.LIQUIDATOR_ROLE)
{
if (vaults.length == 0) revert Errors.InvalidParam("vaults");
for (uint256 i = 0; i < vaults.length; ++i) {
uint256 gasBefore = gasleft();
IDestinationVault vault = vaults[i];
destinationVaultRegistry.verifyIsRegistered(address(vault));
(uint256[] memory amounts, address[] memory tokens) = vault.collectRewards();
uint256 tokensLength = tokens.length;
for (uint256 j = 0; j < tokensLength; ++j) {
address token = tokens[j];
uint256 amount = amounts[j];
if (amount > 0 && token != address(0)) {
// slither-disable-next-line reentrancy-no-eth
_increaseBalance(address(token), address(vault), amount);
}
}
uint256 gasUsed = gasBefore - gasleft();
emit GasUsedForVault(address(vault), gasUsed, bytes32("claim"));
}
}
/// @inheritdoc ILiquidationRow
function balanceOf(address tokenAddress, address vaultAddress) external view returns (uint256) {
return balances[tokenAddress][vaultAddress];
}
/// @inheritdoc ILiquidationRow
function totalBalanceOf(address tokenAddress) external view returns (uint256) {
return totalTokenBalances[tokenAddress];
}
/// @inheritdoc ILiquidationRow
function getTokens() external view returns (address[] memory) {
return rewardTokens.values();
}
/// @inheritdoc ILiquidationRow
function getVaultsForToken(address tokenAddress) external view returns (address[] memory) {
return tokenVaults[tokenAddress].values();
}
/**
* @notice Conducts the liquidation process for a specific token across a list of vaults,
* performing the necessary balance adjustments, initiating the swap process via the asyncSwapper,
* taking a fee from the received amount, and queues the remaining swapped tokens in the MainRewarder associated
* with
* each vault.
* @dev This function calls the _prepareForLiquidation and _performLiquidation functions. These helper functions
* were created to avoid a "stack too deep" error. These functions should only be used within the context of this
* function.
* @param fromToken The token that needs to be liquidated
* @param asyncSwapper The address of the async swapper contract
* @param vaultsToLiquidate The list of vaults that need to be liquidated
* @param params Parameters for the async swap
*/
function liquidateVaultsForToken(
address fromToken,
address asyncSwapper,
IDestinationVault[] memory vaultsToLiquidate,
SwapParams memory params
) external nonReentrant hasRole(Roles.LIQUIDATOR_ROLE) onlyWhitelistedSwapper(asyncSwapper) {
uint256 gasBefore = gasleft();
(uint256 totalBalanceToLiquidate, uint256[] memory vaultsBalances) =
_prepareForLiquidation(fromToken, vaultsToLiquidate);
_performLiquidation(
gasBefore, fromToken, asyncSwapper, vaultsToLiquidate, params, totalBalanceToLiquidate, vaultsBalances
);
}
/**
* @notice Calculates the total balance to liquidate, adjusts the contract state accordingly and calculates fees
* @dev This function is part of a workaround for the "stack too deep" error and is meant to be used with
* _performLiquidation. It is not designed to be used standalone, but as part of the liquidateVaultsForToken
* function
* @param fromToken The token that needs to be liquidated
* @param vaultsToLiquidate The list of vaults that need to be liquidated
* @return totalBalanceToLiquidate The total balance that needs to be liquidated
* @return vaultsBalances The balances of the vaults
*/
function _prepareForLiquidation(
address fromToken,
IDestinationVault[] memory vaultsToLiquidate
) private returns (uint256, uint256[] memory) {
uint256 length = vaultsToLiquidate.length;
uint256 totalBalanceToLiquidate = 0;
uint256[] memory vaultsBalances = new uint256[](length);
for (uint256 i = 0; i < length; ++i) {
address vaultAddress = address(vaultsToLiquidate[i]);
uint256 vaultBalance = balances[fromToken][vaultAddress];
totalBalanceToLiquidate += vaultBalance;
vaultsBalances[i] = vaultBalance;
// Update the total balance for the token
totalTokenBalances[fromToken] -= vaultBalance;
// Update the balance for the vault and token
balances[fromToken][vaultAddress] = 0;
// Remove the vault from the token vaults list
if (!tokenVaults[fromToken].remove(vaultAddress)) revert Errors.ItemNotFound();
}
if (totalBalanceToLiquidate == 0) {
revert NothingToLiquidate();
}
// Check if the token still has any other vaults
if (tokenVaults[fromToken].length() == 0) {
if (!rewardTokens.remove(fromToken)) revert Errors.ItemNotFound();
}
return (totalBalanceToLiquidate, vaultsBalances);
}
/**
* @notice Performs the actual liquidation process, handles the async swap, calculates and transfers the fees,
* and queues the remaining swapped tokens in the MainRewarder associated with each vault.
* @dev This function is part of a workaround for the "stack too deep" error and is meant to be used with
* _prepareForLiquidation. It's not designed to be used standalone, but as part of the liquidateVaultsForToken
* function
* @param gasBefore Amount of gas when the liquidliquidateVaultsForToken function was called
* @param fromToken The token that needs to be liquidated
* @param asyncSwapper The address of the async swapper contract
* @param vaultsToLiquidate The list of vaults that need to be liquidated
* @param params Parameters for the async swap
* @param totalBalanceToLiquidate The total balance that needs to be liquidated
* @param vaultsBalances The balances of the vaults
*/
function _performLiquidation(
uint256 gasBefore,
address fromToken,
address asyncSwapper,
IDestinationVault[] memory vaultsToLiquidate,
SwapParams memory params,
uint256 totalBalanceToLiquidate,
uint256[] memory vaultsBalances
) private {
uint256 length = vaultsToLiquidate.length;
// the swapper checks that the amount received is greater or equal than the params.buyAmount
uint256 amountReceived = IAsyncSwapper(asyncSwapper).swap(params);
// if the fee feature is turned on, send the fee to the fee receiver
if (feeReceiver != address(0) && feeBps > 0) {
uint256 fee = calculateFee(amountReceived);
emit FeesTransfered(feeReceiver, amountReceived, fee);
// adjust the amount received after deducting the fee
amountReceived -= fee;
// transfer fee to the fee receiver
IERC20(params.buyTokenAddress).safeTransfer(feeReceiver, fee);
}
uint256 gasUsedPerVault = (gasBefore - gasleft()) / vaultsToLiquidate.length;
for (uint256 i = 0; i < length; ++i) {
IDestinationVault vaultAddress = vaultsToLiquidate[i];
IMainRewarder mainRewarder = IMainRewarder(vaultAddress.rewarder());
if (mainRewarder.rewardToken() != params.buyTokenAddress) {
revert InvalidRewardToken();
}
uint256 amount = amountReceived * vaultsBalances[i] / totalBalanceToLiquidate;
// approve main rewarder to pull the tokens
LibAdapter._approve(IERC20(params.buyTokenAddress), address(mainRewarder), amount);
mainRewarder.queueNewRewards(amount);
emit VaultLiquidated(address(vaultAddress), fromToken, params.buyTokenAddress, amount);
emit GasUsedForVault(address(vaultAddress), gasUsedPerVault, bytes32("liquidation"));
}
}
/**
* @notice Update the balance of a specific token and vault
* @param tokenAddress The address of the token
* @param vaultAddress The address of the vault
* @param balance The amount of the token to be updated
*/
function _increaseBalance(address tokenAddress, address vaultAddress, uint256 balance) internal {
Errors.verifyNotZero(balance, "balance");
uint256 currentBalance = balances[tokenAddress][vaultAddress];
uint256 totalBalance = totalTokenBalances[tokenAddress];
uint256 newTotalBalance = totalBalance + balance;
// ensure that this contract has enough balance to cover the new total balance
uint256 balanceOfToken = IERC20(tokenAddress).balanceOf(address(this));
if (newTotalBalance > balanceOfToken) {
/**
* @dev This should never happen, but just in case. The error is raised if the updated total balance of a
* specific token in the contract is greater than the actual balance of that token held by the
* contract.
* The calling contract should transfer the funds first before updating the balance.
*/
revert Errors.InsufficientBalance(tokenAddress);
}
// if currentBalance is 0, then the vault is not yet added to the token vaults list
if (currentBalance == 0) {
if (!tokenVaults[tokenAddress].add(vaultAddress)) revert Errors.ItemExists();
if (totalBalance == 0) {
if (!rewardTokens.add(tokenAddress)) revert Errors.ItemExists();
}
}
// Update the total balance for the token
totalTokenBalances[tokenAddress] = newTotalBalance;
// Update the balance for the vault and token
balances[tokenAddress][vaultAddress] = currentBalance + balance;
emit BalanceUpdated(tokenAddress, vaultAddress, currentBalance + balance);
}
}