-
Notifications
You must be signed in to change notification settings - Fork 81
Expand file tree
/
Copy pathSablierBob.sol
More file actions
543 lines (454 loc) · 21.6 KB
/
Copy pathSablierBob.sol
File metadata and controls
543 lines (454 loc) · 21.6 KB
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
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;
import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { Comptrollerable } from "@sablier/evm-utils/src/Comptrollerable.sol";
import { ISablierComptroller } from "@sablier/evm-utils/src/interfaces/ISablierComptroller.sol";
import { SafeOracle } from "@sablier/evm-utils/src/libraries/SafeOracle.sol";
import { SafeTokenSymbol } from "@sablier/evm-utils/src/libraries/SafeTokenSymbol.sol";
import { SablierBobState } from "./abstracts/SablierBobState.sol";
import { BobVaultShare } from "./BobVaultShare.sol";
import { IWETH9 } from "./interfaces/external/IWETH9.sol";
import { IBobVaultShare } from "./interfaces/IBobVaultShare.sol";
import { ISablierBob } from "./interfaces/ISablierBob.sol";
import { ISablierBobAdapter } from "./interfaces/ISablierBobAdapter.sol";
import { Errors } from "./libraries/Errors.sol";
import { Bob } from "./types/Bob.sol";
/*
███████╗ █████╗ ██████╗ ██╗ ██╗███████╗██████╗ ██████╗ ██████╗ ██████╗
██╔════╝██╔══██╗██╔══██╗██║ ██║██╔════╝██╔══██╗ ██╔══██╗██╔═══██╗██╔══██╗
███████╗███████║██████╔╝██║ ██║█████╗ ██████╔╝ ██████╔╝██║ ██║██████╔╝
╚════██║██╔══██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗ ██╔══██╗██║ ██║██╔══██╗
███████║██║ ██║██████╔╝███████╗██║███████╗██║ ██║ ██████╔╝╚██████╔╝██████╔╝
╚══════╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝
*/
/// @title SablierBob
/// @notice See the documentation in {ISablierBob}.
contract SablierBob is
Comptrollerable, // 1 inherited component
ISablierBob, // 1 inherited component
ReentrancyGuard, // 1 inherited component
SablierBobState // 1 inherited component
{
using SafeCast for uint256;
using SafeERC20 for IERC20;
using SafeTokenSymbol for address;
using Strings for uint256;
/*//////////////////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////////////////*/
/// @dev Modifier to check that the vault is active.
modifier onlyActive(uint256 vaultId) {
_revertIfSettledOrExpired(vaultId);
_;
}
/*//////////////////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////////////////*/
/// @param initialComptroller The address of the initial comptroller contract.
constructor(address initialComptroller) Comptrollerable(initialComptroller) SablierBobState() { }
/*//////////////////////////////////////////////////////////////////////////
USER-FACING READ-ONLY FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
/// @inheritdoc ISablierBob
function calculateMinFeeWei(uint256 vaultId) external view override notNull(vaultId) returns (uint256 minFeeWei) {
// Return 0 if the vault has an adapter, since the fee is taken from yield generated by the adapter.
if (address(_vaults[vaultId].adapter) != address(0)) {
return 0;
}
// Calculate the minimum fee in wei for the Bob protocol.
minFeeWei = comptroller.calculateMinFeeWei({ protocol: ISablierComptroller.Protocol.Bob });
}
/*//////////////////////////////////////////////////////////////////////////
USER-FACING STATE-CHANGING FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
/// @inheritdoc ISablierBob
function createVault(
IERC20 token,
AggregatorV3Interface oracle,
uint40 expiry,
uint128 targetPrice
)
external
override
returns (uint256 vaultId)
{
// Check: token is not the zero address.
if (address(token) == address(0)) {
revert Errors.SablierBob_TokenAddressZero();
}
// Check: token is not the native token.
if (address(token) == nativeToken) {
revert Errors.SablierBob_ForbidNativeToken(address(token));
}
uint40 currentTimestamp = uint40(block.timestamp);
// Check: expiry is in the future.
if (expiry <= currentTimestamp) {
revert Errors.SablierBob_ExpiryNotInFuture(expiry, currentTimestamp);
}
// Check: target price is not zero.
if (targetPrice == 0) {
revert Errors.SablierBob_TargetPriceZero();
}
// Check: oracle implements the Chainlink {AggregatorV3Interface} interface.
uint128 latestPrice = SafeOracle.validateOracle(oracle);
// Check: target price is greater than latest oracle price.
if (targetPrice <= latestPrice) {
revert Errors.SablierBob_TargetPriceTooLow(targetPrice, latestPrice);
}
// Load the vault ID from storage.
vaultId = nextVaultId;
// Effect: bump the next vault ID.
unchecked {
nextVaultId = vaultId + 1;
}
// Retrieve token symbol and token decimal.
string memory tokenSymbol = address(token).safeTokenSymbol();
uint8 tokenDecimals = IERC20Metadata(address(token)).decimals();
// Effect: deploy the share token for this vault.
IBobVaultShare shareToken = new BobVaultShare({
name_: string.concat("Sablier Bob ", tokenSymbol, " Vault #", vaultId.toString()),
symbol_: string.concat(
tokenSymbol,
"-",
uint256(targetPrice).toString(),
"-",
uint256(expiry).toString(),
"-",
vaultId.toString()
),
decimals_: tokenDecimals,
sablierBob: address(this),
vaultId: vaultId
});
// Copy the adapter from storage to memory.
ISablierBobAdapter adapter = _defaultAdapters[token];
// Effect: create the vault.
_vaults[vaultId] = Bob.Vault({
token: token,
expiry: expiry,
lastSyncedAt: currentTimestamp,
shareToken: shareToken,
oracle: oracle,
adapter: adapter,
isStakedInAdapter: false,
targetPrice: targetPrice,
lastSyncedPrice: latestPrice
});
// Interaction: register the vault with the adapter.
if (address(adapter) != address(0)) {
adapter.registerVault(vaultId);
// Effect: mark the vault as staked in the adapter.
_vaults[vaultId].isStakedInAdapter = true;
}
// Log the event.
emit CreateVault(vaultId, token, oracle, adapter, shareToken, targetPrice, expiry);
}
/// @inheritdoc ISablierBob
function enter(uint256 vaultId, uint128 amount)
external
override
nonReentrant
notNull(vaultId)
onlyActive(vaultId)
{
// Enter the vault.
_enter({ vaultId: vaultId, from: msg.sender, amount: amount, token: _vaults[vaultId].token });
}
/// @inheritdoc ISablierBob
function enterWithNativeToken(uint256 vaultId)
external
payable
override
nonReentrant
notNull(vaultId)
onlyActive(vaultId)
{
// Cache the vault's token.
address token = address(_vaults[vaultId].token);
// Interaction: call the deposit function in the vault tokens assuming it follows the IWETH9 interface.
// Otherwise, it will revert.
IWETH9(token).deposit{ value: msg.value }();
// Cast `msg.value` to `uint128`.
uint128 amount = msg.value.toUint128();
// Enter the vault.
_enter({ vaultId: vaultId, from: address(this), amount: amount, token: IERC20(token) });
}
/// @inheritdoc ISablierBob
function redeem(uint256 vaultId)
external
payable
override
nonReentrant
notNull(vaultId)
returns (uint128 transferAmount, uint128 feeAmountDeductedFromYield)
{
// If the vault is active, sync the price from the oracle to update the status.
if (_statusOf(vaultId) == Bob.Status.ACTIVE) {
// Effect: sync the price from oracle.
_syncPriceFromOracle(vaultId);
// If it's still active after the sync, revert.
if (_statusOf(vaultId) == Bob.Status.ACTIVE) {
revert Errors.SablierBob_VaultStillActive(vaultId);
}
// Otherwise, the vault has been settled.
}
// Cache storage variables.
ISablierBobAdapter adapter = _vaults[vaultId].adapter;
IBobVaultShare shareToken = _vaults[vaultId].shareToken;
IERC20 token = _vaults[vaultId].token;
// Get the caller's share balance.
uint128 shareBalance = shareToken.balanceOf(msg.sender).toUint128();
// Check: the share balance is not zero.
if (shareBalance == 0) {
revert Errors.SablierBob_NoSharesToRedeem(vaultId, msg.sender);
}
// Check if the vault has an adapter.
if (address(adapter) != address(0)) {
// Check: the `msg.value` is zero.
if (msg.value > 0) {
revert Errors.SablierBob_MsgValueNotZero(vaultId);
}
// Check: the deposit token is staked with the adapter.
if (_vaults[vaultId].isStakedInAdapter) {
// Effect: set isStakedInAdapter to false.
_vaults[vaultId].isStakedInAdapter = false;
// Interaction: unstake all tokens via the adapter.
_unstakeFullAmountViaAdapter(vaultId, adapter);
}
// Interaction: Get the transfer amount and the fee deducted from yield.
(transferAmount, feeAmountDeductedFromYield) = adapter.processRedemption(vaultId, msg.sender, shareBalance);
// Interaction: transfer the fee to the comptroller address.
if (feeAmountDeductedFromYield > 0) {
token.safeTransfer(address(comptroller), feeAmountDeductedFromYield);
}
}
// Otherwise, check that `msg.value` is greater than or equal to the minimum fee required.
else {
// Get the minimum fee from the comptroller.
ISablierComptroller _comptroller = comptroller;
uint256 minFeeWei = _comptroller.calculateMinFeeWei({ protocol: ISablierComptroller.Protocol.Bob });
// Check: `msg.value` is greater than or equal to the minimum fee.
if (msg.value < minFeeWei) {
revert Errors.SablierBob_InsufficientFeePayment(msg.value, minFeeWei);
}
// Interaction: forward native token fee to comptroller.
if (msg.value > 0) {
(bool success,) = address(_comptroller).call{ value: msg.value }("");
if (!success) {
revert Errors.SablierBob_NativeFeeTransferFailed();
}
}
// Return the transfer amount.
transferAmount = shareBalance;
}
// Interaction: burn share tokens from the caller.
shareToken.burn(vaultId, msg.sender, shareBalance);
// Interaction: transfer tokens to the caller.
token.safeTransfer(msg.sender, transferAmount);
// Log the event.
emit Redeem({
vaultId: vaultId,
user: msg.sender,
amountReceived: transferAmount,
sharesBurned: shareBalance,
fee: feeAmountDeductedFromYield
});
}
/// @inheritdoc ISablierBob
function setNativeToken(address newNativeToken) external override onlyComptroller {
// Check: provided token is not zero address.
if (newNativeToken == address(0)) {
revert Errors.SablierBob_NativeTokenZeroAddress();
}
// Check: native token is not set.
if (nativeToken != address(0)) {
revert Errors.SablierBob_NativeTokenAlreadySet(nativeToken);
}
// Effect: set the native token.
nativeToken = newNativeToken;
// Log the update.
emit SetNativeToken({ comptroller: msg.sender, nativeToken: newNativeToken });
}
/// @inheritdoc ISablierBob
function setDefaultAdapter(IERC20 token, ISablierBobAdapter newAdapter) external override onlyComptroller {
// Check: the new adapter implements the {ISablierBobAdapter} interface.
if (address(newAdapter) != address(0)) {
bytes4 interfaceId = type(ISablierBobAdapter).interfaceId;
if (!IERC165(address(newAdapter)).supportsInterface(interfaceId)) {
revert Errors.SablierBob_NewAdapterMissesInterface(address(newAdapter));
}
}
// Effect: set the default adapter for the token.
_defaultAdapters[token] = newAdapter;
// Log the adapter change.
emit SetDefaultAdapter(token, newAdapter);
}
/// @inheritdoc ISablierBob
function syncPriceFromOracle(uint256 vaultId)
external
override
nonReentrant
notNull(vaultId)
onlyActive(vaultId)
returns (uint128 latestPrice)
{
// Effect: sync the price from oracle.
latestPrice = _syncPriceFromOracle(vaultId);
}
/// @inheritdoc ISablierBob
function unstakeTokensViaAdapter(uint256 vaultId)
external
override
nonReentrant
notNull(vaultId)
returns (uint128 amountReceivedFromAdapter)
{
// Cache storage variable.
ISablierBobAdapter adapter = _vaults[vaultId].adapter;
// Check: the vault has an adapter.
if (address(adapter) == address(0)) {
revert Errors.SablierBob_VaultHasNoAdapter(vaultId);
}
// Check: the vault has not already been unstaked.
if (!_vaults[vaultId].isStakedInAdapter) {
revert Errors.SablierBob_VaultAlreadyUnstaked(vaultId);
}
// Check: there is something to unstake.
if (adapter.getTotalYieldBearingTokenBalance(vaultId) == 0) {
revert Errors.SablierBob_UnstakeAmountZero(vaultId);
}
// If the vault is active, sync the price from the oracle to update the status.
if (_statusOf(vaultId) == Bob.Status.ACTIVE) {
// Effect: sync the price from oracle.
_syncPriceFromOracle(vaultId);
// If it's still active after the sync, revert.
if (_statusOf(vaultId) == Bob.Status.ACTIVE) {
revert Errors.SablierBob_VaultStillActive(vaultId);
}
// Otherwise, the vault has been settled.
}
// Effect: mark the vault as not staked with the adapter.
_vaults[vaultId].isStakedInAdapter = false;
// Interaction: unstake all tokens via the adapter.
amountReceivedFromAdapter = _unstakeFullAmountViaAdapter(vaultId, adapter);
}
/// @inheritdoc ISablierBob
function onShareTransfer(
uint256 vaultId,
address from,
address to,
uint256 amount,
uint256 fromBalanceBefore
)
external
override
{
// Check: caller is the share token for this vault.
if (msg.sender != address(_vaults[vaultId].shareToken)) {
revert Errors.SablierBob_CallerNotShareToken(vaultId, msg.sender);
}
// Cache storage variable.
ISablierBobAdapter adapter = _vaults[vaultId].adapter;
if (address(adapter) != address(0)) {
// Interaction: update staked token holding of the user in the adapter.
adapter.updateStakedTokenBalance(vaultId, from, to, amount, fromBalanceBefore);
}
}
/*//////////////////////////////////////////////////////////////////////////
PRIVATE STATE-CHANGING FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
/// @dev Common function to enter into a vault by depositing tokens into it and minting share tokens to caller.
/// @param vaultId The ID of the vault to deposit into.
/// @param from The address holding the vault token when calling this function. In case of native token deposits,
/// the vault tokens are held by this contract.
/// @param amount The amount of tokens to deposit.
/// @param token The ERC-20 token accepted by the vault.
function _enter(uint256 vaultId, address from, uint128 amount, IERC20 token) private {
// Check: the deposit amount is not zero.
if (amount == 0) {
revert Errors.SablierBob_DepositAmountZero(vaultId, msg.sender);
}
// Effect: sync the price from oracle.
_syncPriceFromOracle(vaultId);
// Check: the vault is still active after the price sync.
_revertIfSettledOrExpired(vaultId);
// Cache storage variables.
ISablierBobAdapter adapter = _vaults[vaultId].adapter;
// If adapter is set, transfer tokens to the adapter.
if (address(adapter) != address(0)) {
// Interaction: transfer tokens to the adapter. Use `safeTransfer` for the native token path since
// the contract already holds the wrapped tokens.
if (from == address(this)) {
token.safeTransfer(address(adapter), amount);
} else {
token.safeTransferFrom(from, address(adapter), amount);
}
// Interaction: stake tokens via the adapter on behalf of the caller.
adapter.stake(vaultId, msg.sender, amount);
}
// Otherwise, if `from` is `msg.sender`, transfer tokens to this contract. When this function is called by
// `enterWithNativeToken`, the vault tokens are held by this contract already.
else if (from == msg.sender) {
// Interaction: transfer tokens from caller to this contract.
token.safeTransferFrom(from, address(this), amount);
}
// Interaction: mint share tokens to the caller.
_vaults[vaultId].shareToken.mint(vaultId, msg.sender, amount);
// Log the deposit.
emit Enter({ vaultId: vaultId, user: msg.sender, amountReceived: amount, sharesMinted: amount });
}
/// @notice Private function that reverts if the vault is settled or expired.
/// @param vaultId The ID of the vault.
function _revertIfSettledOrExpired(uint256 vaultId) private view {
if (_statusOf(vaultId) != Bob.Status.ACTIVE) {
revert Errors.SablierBob_VaultNotActive(vaultId);
}
}
/// @dev Private function to fetch the latest oracle price and update it in the vault storage.
/// @param vaultId The ID of the vault.
/// @return latestPrice The latest price from the oracle.
function _syncPriceFromOracle(uint256 vaultId) private returns (uint128 latestPrice) {
AggregatorV3Interface oracleAddress = _vaults[vaultId].oracle;
// Get the latest price, normalized to 8 decimals, from the oracle with safety checks.
(latestPrice,,) = SafeOracle.safeOraclePrice({ oracle: oracleAddress, normalize: true });
// Return if the latest price is zero.
if (latestPrice == 0) {
return 0;
}
uint40 currentTimestamp = uint40(block.timestamp);
// Effect: update the last synced price and timestamp.
_vaults[vaultId].lastSyncedPrice = latestPrice;
_vaults[vaultId].lastSyncedAt = currentTimestamp;
// Log the event.
emit SyncPriceFromOracle({
vaultId: vaultId,
oracle: oracleAddress,
latestPrice: latestPrice,
syncedAt: currentTimestamp
});
}
/// @dev Private function to unstake all tokens using the adapter.
/// @param vaultId The ID of the vault.
/// @param adapter The adapter to use for unstaking.
/// @return amountReceivedFromAdapter The amount of tokens received from the adapter after unstaking.
function _unstakeFullAmountViaAdapter(
uint256 vaultId,
ISablierBobAdapter adapter
)
private
returns (uint128 amountReceivedFromAdapter)
{
uint128 wrappedTokenUnstakedAmount;
// Interaction: unstake all tokens via the adapter.
(wrappedTokenUnstakedAmount, amountReceivedFromAdapter) = adapter.unstakeFullAmount(vaultId);
// Log the event.
emit UnstakeFromAdapter(vaultId, adapter, wrappedTokenUnstakedAmount, amountReceivedFromAdapter);
}
}