-
Notifications
You must be signed in to change notification settings - Fork 49
/
Copy pathHATVault.sol
437 lines (377 loc) · 18.3 KB
/
HATVault.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
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
// SPDX-License-Identifier: MIT
// Disclaimer https://github.com/hats-finance/hats-contracts/blob/main/DISCLAIMER.md
pragma solidity 0.8.16;
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./tokenlock/TokenLockFactory.sol";
import "./interfaces/IHATVault.sol";
import "./interfaces/IHATClaimsManager.sol";
import "./interfaces/IRewardController.sol";
import "./HATVaultsRegistry.sol";
/** @title A Hats.finance vault which holds the funds for a specific project's
* bug bounties
* @author Hats.finance
* @notice The HATVault can be deposited into in a permissionless manner using
* the vault’s native token.
*
* Anyone can deposit the vault's native token into the vault and
* recieve shares for it. Shares represent the user's relative part in the
* vault, and when a bounty is paid out, users lose part of their deposits
* (based on percentage paid), but keep their share of the vault.
* Users also receive rewards for their deposits, which can be claimed at any
* time.
* To withdraw previously deposited tokens, a user must first send a withdraw
* request, and the withdrawal will be made available after a pending period.
* Withdrawals are not permitted during safety periods or while there is an
* active claim for a bounty payout.
*
* This project is open-source and can be found at:
* https://github.com/hats-finance/hats-contracts
*/
contract HATVault is IHATVault, ERC4626Upgradeable, OwnableUpgradeable, ReentrancyGuardUpgradeable {
using SafeERC20 for IERC20;
using MathUpgradeable for uint256;
string public constant VERSION = "3.0";
uint256 public constant MAX_UINT = type(uint256).max;
uint256 public constant HUNDRED_PERCENT = 1e4;
uint256 public constant MAX_WITHDRAWAL_FEE = 2e2; // Max fee is 2%
uint256 public constant MINIMAL_AMOUNT_OF_SHARES = 1e3; // to reduce rounding errors, the number of shares is either 0, or > than this number
address public claimsManager;
IHATVaultsRegistry public registry;
// Time of when withdrawal period starts for every user that has an
// active withdraw request. (time when last withdraw request pending
// period ended, or 0 if last action was deposit or withdraw)
mapping(address => uint256) public withdrawEnableStartTime;
IRewardController[] public rewardControllers;
uint256 public withdrawalFee;
bool public vaultStarted;
bool public depositPause;
bool public withdrawPaused;
bool private _isEmergencyWithdraw;
bool private _vaultDestroyed;
modifier onlyClaimsManager() {
if (claimsManager != _msgSender()) revert OnlyClaimsManager();
_;
}
modifier onlyRegistryOwner() {
if (registry.owner() != _msgSender()) revert OnlyRegistryOwner();
_;
}
modifier onlyFeeSetter() {
if (registry.feeSetter() != msg.sender) revert OnlyFeeSetter();
_;
}
/** @notice See {IHATVault-initialize}. */
function initialize(address _claimsManager, IHATVault.VaultInitParams calldata _params) external initializer {
__ERC20_init(string.concat("Hats Vault ", _params.name), string.concat("HAT", _params.symbol));
__ERC4626_init(IERC20MetadataUpgradeable(address(_params.asset)));
__ReentrancyGuard_init();
_transferOwnership(_params.owner);
for (uint256 i = 0; i < _params.rewardControllers.length;) {
_addRewardController(_params.rewardControllers[i]);
unchecked { ++i; }
}
claimsManager = _claimsManager;
depositPause = _params.isPaused;
registry = IHATVaultsRegistry(_msgSender());
emit SetVaultDescription(_params.descriptionHash);
}
/** @notice See {IHATVault-approveClaim}. */
function makePayout(uint256 _amount) external onlyClaimsManager {
IERC20(asset()).safeTransfer(address(_msgSender()), _amount);
emit VaultPayout(_amount);
}
/** @notice See {IHATVault-setWithdrawPaused}. */
function setWithdrawPaused(bool _withdrawPaused) external onlyClaimsManager {
withdrawPaused = _withdrawPaused;
emit SetWithdrawPaused(_withdrawPaused);
}
/** @notice See {IHATVault-destroyVault}. */
function startVault() external onlyClaimsManager {
vaultStarted = true;
emit VaultStarted();
}
/** @notice See {IHATVault-destroyVault}. */
function destroyVault() external onlyClaimsManager {
depositPause = true;
_vaultDestroyed = true;
emit VaultDestroyed();
}
/** @notice See {IHATVault-addRewardController}. */
function addRewardController(IRewardController _rewardController) external onlyRegistryOwner {
_addRewardController(_rewardController);
}
/** @notice See {IHATVault-setDepositPause}. */
function setDepositPause(bool _depositPause) external onlyOwner {
if (_vaultDestroyed)
revert CannotUnpauseDestroyedVault();
depositPause = _depositPause;
emit SetDepositPause(_depositPause);
}
/** @notice See {IHATVault-setVaultDescription}. */
function setVaultDescription(string calldata _descriptionHash) external onlyRegistryOwner {
emit SetVaultDescription(_descriptionHash);
}
/** @notice See {IHATVault-setWithdrawalFee}. */
function setWithdrawalFee(uint256 _fee) external onlyFeeSetter {
if (_fee > MAX_WITHDRAWAL_FEE) revert WithdrawalFeeTooBig();
withdrawalFee = _fee;
emit SetWithdrawalFee(_fee);
}
/* -------------------------------------------------------------------------------- */
/* ---------------------------------- Vault --------------------------------------- */
/** @notice See {IHATVault-withdrawRequest}. */
function withdrawRequest() external nonReentrant {
// set the withdrawEnableStartTime time to be withdrawRequestPendingPeriod from now
// solhint-disable-next-line not-rely-on-time
uint256 _withdrawEnableStartTime = block.timestamp + registry.getWithdrawRequestPendingPeriod();
address msgSender = _msgSender();
withdrawEnableStartTime[msgSender] = _withdrawEnableStartTime;
emit WithdrawRequest(msgSender, _withdrawEnableStartTime);
}
/** @notice See {IHATVault-withdrawAndClaim}. */
function withdrawAndClaim(uint256 assets, address receiver, address owner) external returns (uint256 shares) {
shares = withdraw(assets, receiver, owner);
_claimRewards(owner);
}
/** @notice See {IHATVault-redeemAndClaim}. */
function redeemAndClaim(uint256 shares, address receiver, address owner) external returns (uint256 assets) {
assets = redeem(shares, receiver, owner);
_claimRewards(owner);
}
/** @notice See {IHATVault-emergencyWithdraw}. */
function emergencyWithdraw(address receiver) external returns (uint256 assets) {
_isEmergencyWithdraw = true;
address msgSender = _msgSender();
assets = redeem(balanceOf(msgSender), receiver, msgSender);
_isEmergencyWithdraw = false;
}
/** @notice See {IHATVault-withdraw}. */
function withdraw(uint256 assets, address receiver, address owner)
public override(IHATVault, ERC4626Upgradeable) virtual returns (uint256) {
(uint256 _shares, uint256 _fee) = previewWithdrawAndFee(assets);
_withdraw(_msgSender(), receiver, owner, assets, _shares, _fee);
return _shares;
}
/** @notice See {IHATVault-redeem}. */
function redeem(uint256 shares, address receiver, address owner)
public override(IHATVault, ERC4626Upgradeable) virtual returns (uint256) {
(uint256 _assets, uint256 _fee) = previewRedeemAndFee(shares);
_withdraw(_msgSender(), receiver, owner, _assets, shares, _fee);
return _assets;
}
/** @notice See {IHATVault-deposit}. */
function deposit(uint256 assets, address receiver) public override(IHATVault, ERC4626Upgradeable) virtual returns (uint256) {
return super.deposit(assets, receiver);
}
/** @notice See {IHATVault-withdraw}. */
function withdraw(uint256 assets, address receiver, address owner, uint256 maxShares) public virtual returns (uint256) {
uint256 shares = withdraw(assets, receiver, owner);
if (shares > maxShares) revert WithdrawSlippageProtection();
return shares;
}
/** @notice See {IHATVault-redeem}. */
function redeem(uint256 shares, address receiver, address owner, uint256 minAssets) public virtual returns (uint256) {
uint256 assets = redeem(shares, receiver, owner);
if (assets < minAssets) revert RedeemSlippageProtection();
return assets;
}
/** @notice See {IHATVault-withdrawAndClaim}. */
function withdrawAndClaim(uint256 assets, address receiver, address owner, uint256 maxShares) external returns (uint256 shares) {
shares = withdraw(assets, receiver, owner, maxShares);
_claimRewards(owner);
}
/** @notice See {IHATVault-redeemAndClaim}. */
function redeemAndClaim(uint256 shares, address receiver, address owner, uint256 minAssets) external returns (uint256 assets) {
assets = redeem(shares, receiver, owner, minAssets);
_claimRewards(owner);
}
/** @notice See {IHATVault-deposit}. */
function deposit(uint256 assets, address receiver, uint256 minShares) external virtual returns (uint256) {
uint256 shares = deposit(assets, receiver);
if (shares < minShares) revert DepositSlippageProtection();
return shares;
}
/** @notice See {IHATVault-mint}. */
function mint(uint256 shares, address receiver, uint256 maxAssets) external virtual returns (uint256) {
uint256 assets = mint(shares, receiver);
if (assets > maxAssets) revert MintSlippageProtection();
return assets;
}
/** @notice See {IERC4626Upgradeable-maxDeposit}. */
function maxDeposit(address) public view virtual override(IERC4626Upgradeable, ERC4626Upgradeable) returns (uint256) {
return depositPause ? 0 : MAX_UINT;
}
/** @notice See {IERC4626Upgradeable-maxMint}. */
function maxMint(address) public view virtual override(IERC4626Upgradeable, ERC4626Upgradeable) returns (uint256) {
return depositPause ? 0 : MAX_UINT;
}
/** @notice See {IERC4626Upgradeable-maxWithdraw}. */
function maxWithdraw(address owner) public view virtual override(IERC4626Upgradeable, ERC4626Upgradeable) returns (uint256) {
if (withdrawPaused || !_isWithdrawEnabledForUser(owner)) return 0;
return previewRedeem(balanceOf(owner));
}
/** @notice See {IERC4626Upgradeable-maxRedeem}. */
function maxRedeem(address owner) public view virtual override(IERC4626Upgradeable, ERC4626Upgradeable) returns (uint256) {
if (withdrawPaused || !_isWithdrawEnabledForUser(owner)) return 0;
return balanceOf(owner);
}
/** @notice See {IERC4626Upgradeable-previewWithdraw}. */
function previewWithdraw(uint256 assets) public view virtual override(IERC4626Upgradeable, ERC4626Upgradeable) returns (uint256 shares) {
(shares,) = previewWithdrawAndFee(assets);
}
/** @notice See {IERC4626Upgradeable-previewRedeem}. */
function previewRedeem(uint256 shares) public view virtual override(IERC4626Upgradeable, ERC4626Upgradeable) returns (uint256 assets) {
(assets,) = previewRedeemAndFee(shares);
}
/** @notice See {IHATVault-previewWithdrawAndFee}. */
function previewWithdrawAndFee(uint256 assets) public view returns (uint256 shares, uint256 fee) {
uint256 _withdrawalFee = withdrawalFee;
fee = assets.mulDiv(_withdrawalFee, (HUNDRED_PERCENT - _withdrawalFee));
shares = _convertToShares(assets + fee, MathUpgradeable.Rounding.Up);
}
/** @notice See {IHATVault-previewRedeemAndFee}. */
function previewRedeemAndFee(uint256 shares) public view returns (uint256 assets, uint256 fee) {
uint256 _assetsPlusFee = _convertToAssets(shares, MathUpgradeable.Rounding.Down);
fee = _assetsPlusFee.mulDiv(withdrawalFee, HUNDRED_PERCENT);
unchecked { // fee will always be maximun 20% of _assetsPlusFee
assets = _assetsPlusFee - fee;
}
}
/* -------------------------------------------------------------------------------- */
/* --------------------------------- Helpers -------------------------------------- */
/**
* @dev Deposit funds to the vault. Can only be called if the committee had
* checked in and deposits are not paused.
* @param caller Caller of the action (msg.sender)
* @param receiver Reciever of the shares from the deposit
* @param assets Amount of vault's native token to deposit
* @param shares Respective amount of shares to be received
*/
function _deposit(
address caller,
address receiver,
uint256 assets,
uint256 shares
) internal override virtual nonReentrant {
if (!vaultStarted)
revert VaultNotStartedYet();
if (receiver == caller && withdrawEnableStartTime[receiver] != 0 ) {
// clear withdraw request if caller deposits in her own account
withdrawEnableStartTime[receiver] = 0;
}
super._deposit(caller, receiver, assets, shares);
}
// amount of shares correspond with assets + fee
function _withdraw(
address _caller,
address _receiver,
address _owner,
uint256 _assets,
uint256 _shares,
uint256 _fee
) internal nonReentrant {
if (_assets == 0) revert WithdrawMustBeGreaterThanZero();
if (_caller != _owner) {
_spendAllowance(_owner, _caller, _shares);
}
_burn(_owner, _shares);
IERC20 _asset = IERC20(asset());
if (_fee > 0) {
_asset.safeTransfer(registry.owner(), _fee);
}
_asset.safeTransfer(_receiver, _assets);
emit Withdraw(_caller, _receiver, _owner, _assets, _shares);
}
/**
* @dev Claim rewards from the vault's reward controllers for the owner
* @param owner The owner of the rewards to claim for
*/
function _claimRewards(address owner) internal {
for (uint256 i = 0; i < rewardControllers.length;) {
rewardControllers[i].claimReward(address(this), owner);
unchecked { ++i; }
}
}
function _beforeTokenTransfer(
address _from,
address _to,
uint256 _amount
) internal virtual override {
if (_amount == 0) revert AmountCannotBeZero();
if (_from == _to) revert CannotTransferToSelf();
// deposit/mint/transfer
if (_to != address(0)) {
IHATVaultsRegistry _registry = registry;
if (_registry.isEmergencyPaused()) revert SystemInEmergencyPause();
// Cannot transfer or mint tokens to a user for which an active withdraw request exists
// because then we would need to reset their withdraw request
uint256 _withdrawEnableStartTime = withdrawEnableStartTime[_to];
if (_withdrawEnableStartTime != 0) {
// solhint-disable-next-line not-rely-on-time
if (block.timestamp <= _withdrawEnableStartTime + _registry.getWithdrawRequestEnablePeriod())
revert CannotTransferToAnotherUserWithActiveWithdrawRequest();
}
for (uint256 i = 0; i < rewardControllers.length;) {
rewardControllers[i].commitUserBalance(_to, _amount, true);
unchecked { ++i; }
}
}
// withdraw/redeem/transfer
if (_from != address(0)) {
if (_amount > maxRedeem(_from)) revert RedeemMoreThanMax();
// if all is ok and withdrawal can be made -
// reset withdrawRequests[_pid][msg.sender] so that another withdrawRequest
// will have to be made before next withdrawal
withdrawEnableStartTime[_from] = 0;
if (!_isEmergencyWithdraw) {
for (uint256 i = 0; i < rewardControllers.length;) {
rewardControllers[i].commitUserBalance(_from, _amount, false);
unchecked { ++i; }
}
}
}
}
function _afterTokenTransfer(address, address, uint256) internal virtual override {
if (totalSupply() > 0 && totalSupply() < MINIMAL_AMOUNT_OF_SHARES) {
revert AmountOfSharesMustBeMoreThanMinimalAmount();
}
}
/**
* @dev Checks that the given user can perform a withdraw at this time
* @param _user Address of the user to check
*/
function _isWithdrawEnabledForUser(address _user)
internal view
returns(bool)
{
IHATVaultsRegistry _registry = registry;
uint256 _withdrawPeriod = _registry.getWithdrawPeriod();
// disable withdraw for safetyPeriod (e.g 1 hour) after each withdrawPeriod (e.g 11 hours)
// solhint-disable-next-line not-rely-on-time
if (block.timestamp % (_withdrawPeriod + _registry.getSafetyPeriod()) >= _withdrawPeriod)
return false;
// check that withdrawRequestPendingPeriod had passed
uint256 _withdrawEnableStartTime = withdrawEnableStartTime[_user];
// solhint-disable-next-line not-rely-on-time
return (block.timestamp >= _withdrawEnableStartTime &&
// check that withdrawRequestEnablePeriod had not passed and that the
// last action was withdrawRequest (and not deposit or withdraw, which
// reset withdrawRequests[_user] to 0)
// solhint-disable-next-line not-rely-on-time
block.timestamp <= _withdrawEnableStartTime + _registry.getWithdrawRequestEnablePeriod());
}
function _addRewardController(IRewardController _rewardController) internal {
for (uint256 i = 0; i < rewardControllers.length;) {
if (_rewardController == rewardControllers[i]) revert DuplicatedRewardController();
unchecked { ++i; }
}
rewardControllers.push(_rewardController);
emit AddRewardController(_rewardController);
}
/* -------------------------------------------------------------------------------- */
}