@sablier/bob 1.0.1

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE-BUSL.md +82 -0
  3. package/LICENSE-GPL.md +470 -0
  4. package/LICENSE.md +9 -0
  5. package/README.md +81 -0
  6. package/artifacts/BobVaultShare.json +936 -0
  7. package/artifacts/SablierBob.json +1683 -0
  8. package/artifacts/SablierEscrow.json +1316 -0
  9. package/artifacts/SablierLidoAdapter.json +1649 -0
  10. package/artifacts/erc20/IERC20.json +226 -0
  11. package/artifacts/interfaces/IBobVaultShare.json +393 -0
  12. package/artifacts/interfaces/ISablierBob.json +1171 -0
  13. package/artifacts/interfaces/ISablierEscrow.json +999 -0
  14. package/artifacts/interfaces/ISablierLidoAdapter.json +1141 -0
  15. package/artifacts/interfaces/external/ICurveStETHPool.json +128 -0
  16. package/artifacts/interfaces/external/ILidoWithdrawalQueue.json +209 -0
  17. package/artifacts/interfaces/external/IStETH.json +262 -0
  18. package/artifacts/interfaces/external/IWETH9.json +259 -0
  19. package/artifacts/interfaces/external/IWstETH.json +311 -0
  20. package/artifacts/libraries/Errors.json +868 -0
  21. package/package.json +68 -0
  22. package/src/BobVaultShare.sol +119 -0
  23. package/src/SablierBob.sol +543 -0
  24. package/src/SablierEscrow.sol +288 -0
  25. package/src/SablierLidoAdapter.sol +549 -0
  26. package/src/abstracts/SablierBobState.sol +156 -0
  27. package/src/abstracts/SablierEscrowState.sol +159 -0
  28. package/src/interfaces/IBobVaultShare.sol +51 -0
  29. package/src/interfaces/ISablierBob.sol +261 -0
  30. package/src/interfaces/ISablierBobAdapter.sol +157 -0
  31. package/src/interfaces/ISablierBobState.sol +74 -0
  32. package/src/interfaces/ISablierEscrow.sol +148 -0
  33. package/src/interfaces/ISablierEscrowState.sol +77 -0
  34. package/src/interfaces/ISablierLidoAdapter.sol +110 -0
  35. package/src/interfaces/external/ICurveStETHPool.sol +31 -0
  36. package/src/interfaces/external/ILidoWithdrawalQueue.sol +67 -0
  37. package/src/interfaces/external/IStETH.sol +18 -0
  38. package/src/interfaces/external/IWETH9.sol +19 -0
  39. package/src/interfaces/external/IWstETH.sol +32 -0
  40. package/src/libraries/Errors.sol +189 -0
  41. package/src/types/Bob.sol +49 -0
  42. package/src/types/Escrow.sol +49 -0
@@ -0,0 +1,543 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ pragma solidity >=0.8.22;
3
+
4
+ import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
5
+ import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
6
+ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7
+ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
8
+ import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
9
+ import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
10
+ import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
11
+ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
12
+ import { Comptrollerable } from "@sablier/evm-utils/src/Comptrollerable.sol";
13
+ import { ISablierComptroller } from "@sablier/evm-utils/src/interfaces/ISablierComptroller.sol";
14
+ import { SafeOracle } from "@sablier/evm-utils/src/libraries/SafeOracle.sol";
15
+ import { SafeTokenSymbol } from "@sablier/evm-utils/src/libraries/SafeTokenSymbol.sol";
16
+ import { SablierBobState } from "./abstracts/SablierBobState.sol";
17
+ import { BobVaultShare } from "./BobVaultShare.sol";
18
+ import { IWETH9 } from "./interfaces/external/IWETH9.sol";
19
+ import { IBobVaultShare } from "./interfaces/IBobVaultShare.sol";
20
+ import { ISablierBob } from "./interfaces/ISablierBob.sol";
21
+ import { ISablierBobAdapter } from "./interfaces/ISablierBobAdapter.sol";
22
+ import { Errors } from "./libraries/Errors.sol";
23
+ import { Bob } from "./types/Bob.sol";
24
+
25
+ /*
26
+
27
+ ███████╗ █████╗ ██████╗ ██╗ ██╗███████╗██████╗ ██████╗ ██████╗ ██████╗
28
+ ██╔════╝██╔══██╗██╔══██╗██║ ██║██╔════╝██╔══██╗ ██╔══██╗██╔═══██╗██╔══██╗
29
+ ███████╗███████║██████╔╝██║ ██║█████╗ ██████╔╝ ██████╔╝██║ ██║██████╔╝
30
+ ╚════██║██╔══██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗ ██╔══██╗██║ ██║██╔══██╗
31
+ ███████║██║ ██║██████╔╝███████╗██║███████╗██║ ██║ ██████╔╝╚██████╔╝██████╔╝
32
+ ╚══════╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝
33
+
34
+ */
35
+
36
+ /// @title SablierBob
37
+ /// @notice See the documentation in {ISablierBob}.
38
+ contract SablierBob is
39
+ Comptrollerable, // 1 inherited component
40
+ ISablierBob, // 1 inherited component
41
+ ReentrancyGuard, // 1 inherited component
42
+ SablierBobState // 1 inherited component
43
+ {
44
+ using SafeCast for uint256;
45
+ using SafeERC20 for IERC20;
46
+ using SafeTokenSymbol for address;
47
+ using Strings for uint256;
48
+
49
+ /*//////////////////////////////////////////////////////////////////////////
50
+ MODIFIERS
51
+ //////////////////////////////////////////////////////////////////////////*/
52
+
53
+ /// @dev Modifier to check that the vault is active.
54
+ modifier onlyActive(uint256 vaultId) {
55
+ _revertIfSettledOrExpired(vaultId);
56
+ _;
57
+ }
58
+
59
+ /*//////////////////////////////////////////////////////////////////////////
60
+ CONSTRUCTOR
61
+ //////////////////////////////////////////////////////////////////////////*/
62
+
63
+ /// @param initialComptroller The address of the initial comptroller contract.
64
+ constructor(address initialComptroller) Comptrollerable(initialComptroller) SablierBobState() { }
65
+
66
+ /*//////////////////////////////////////////////////////////////////////////
67
+ USER-FACING READ-ONLY FUNCTIONS
68
+ //////////////////////////////////////////////////////////////////////////*/
69
+
70
+ /// @inheritdoc ISablierBob
71
+ function calculateMinFeeWei(uint256 vaultId) external view override notNull(vaultId) returns (uint256 minFeeWei) {
72
+ // Return 0 if the vault has an adapter, since the fee is taken from yield generated by the adapter.
73
+ if (address(_vaults[vaultId].adapter) != address(0)) {
74
+ return 0;
75
+ }
76
+
77
+ // Calculate the minimum fee in wei for the Bob protocol.
78
+ minFeeWei = comptroller.calculateMinFeeWei({ protocol: ISablierComptroller.Protocol.Bob });
79
+ }
80
+
81
+ /*//////////////////////////////////////////////////////////////////////////
82
+ USER-FACING STATE-CHANGING FUNCTIONS
83
+ //////////////////////////////////////////////////////////////////////////*/
84
+
85
+ /// @inheritdoc ISablierBob
86
+ function createVault(
87
+ IERC20 token,
88
+ AggregatorV3Interface oracle,
89
+ uint40 expiry,
90
+ uint128 targetPrice
91
+ )
92
+ external
93
+ override
94
+ returns (uint256 vaultId)
95
+ {
96
+ // Check: token is not the zero address.
97
+ if (address(token) == address(0)) {
98
+ revert Errors.SablierBob_TokenAddressZero();
99
+ }
100
+
101
+ // Check: token is not the native token.
102
+ if (address(token) == nativeToken) {
103
+ revert Errors.SablierBob_ForbidNativeToken(address(token));
104
+ }
105
+
106
+ uint40 currentTimestamp = uint40(block.timestamp);
107
+
108
+ // Check: expiry is in the future.
109
+ if (expiry <= currentTimestamp) {
110
+ revert Errors.SablierBob_ExpiryNotInFuture(expiry, currentTimestamp);
111
+ }
112
+
113
+ // Check: target price is not zero.
114
+ if (targetPrice == 0) {
115
+ revert Errors.SablierBob_TargetPriceZero();
116
+ }
117
+
118
+ // Check: oracle implements the Chainlink {AggregatorV3Interface} interface.
119
+ uint128 latestPrice = SafeOracle.validateOracle(oracle);
120
+
121
+ // Check: target price is greater than latest oracle price.
122
+ if (targetPrice <= latestPrice) {
123
+ revert Errors.SablierBob_TargetPriceTooLow(targetPrice, latestPrice);
124
+ }
125
+
126
+ // Load the vault ID from storage.
127
+ vaultId = nextVaultId;
128
+
129
+ // Effect: bump the next vault ID.
130
+ unchecked {
131
+ nextVaultId = vaultId + 1;
132
+ }
133
+
134
+ // Retrieve token symbol and token decimal.
135
+ string memory tokenSymbol = address(token).safeTokenSymbol();
136
+ uint8 tokenDecimals = IERC20Metadata(address(token)).decimals();
137
+
138
+ // Effect: deploy the share token for this vault.
139
+ IBobVaultShare shareToken = new BobVaultShare({
140
+ name_: string.concat("Sablier Bob ", tokenSymbol, " Vault #", vaultId.toString()),
141
+ symbol_: string.concat(
142
+ tokenSymbol,
143
+ "-",
144
+ uint256(targetPrice).toString(),
145
+ "-",
146
+ uint256(expiry).toString(),
147
+ "-",
148
+ vaultId.toString()
149
+ ),
150
+ decimals_: tokenDecimals,
151
+ sablierBob: address(this),
152
+ vaultId: vaultId
153
+ });
154
+
155
+ // Copy the adapter from storage to memory.
156
+ ISablierBobAdapter adapter = _defaultAdapters[token];
157
+
158
+ // Effect: create the vault.
159
+ _vaults[vaultId] = Bob.Vault({
160
+ token: token,
161
+ expiry: expiry,
162
+ lastSyncedAt: currentTimestamp,
163
+ shareToken: shareToken,
164
+ oracle: oracle,
165
+ adapter: adapter,
166
+ isStakedInAdapter: false,
167
+ targetPrice: targetPrice,
168
+ lastSyncedPrice: latestPrice
169
+ });
170
+
171
+ // Interaction: register the vault with the adapter.
172
+ if (address(adapter) != address(0)) {
173
+ adapter.registerVault(vaultId);
174
+
175
+ // Effect: mark the vault as staked in the adapter.
176
+ _vaults[vaultId].isStakedInAdapter = true;
177
+ }
178
+
179
+ // Log the event.
180
+ emit CreateVault(vaultId, token, oracle, adapter, shareToken, targetPrice, expiry);
181
+ }
182
+
183
+ /// @inheritdoc ISablierBob
184
+ function enter(uint256 vaultId, uint128 amount)
185
+ external
186
+ override
187
+ nonReentrant
188
+ notNull(vaultId)
189
+ onlyActive(vaultId)
190
+ {
191
+ // Enter the vault.
192
+ _enter({ vaultId: vaultId, from: msg.sender, amount: amount, token: _vaults[vaultId].token });
193
+ }
194
+
195
+ /// @inheritdoc ISablierBob
196
+ function enterWithNativeToken(uint256 vaultId)
197
+ external
198
+ payable
199
+ override
200
+ nonReentrant
201
+ notNull(vaultId)
202
+ onlyActive(vaultId)
203
+ {
204
+ // Cache the vault's token.
205
+ address token = address(_vaults[vaultId].token);
206
+
207
+ // Interaction: call the deposit function in the vault tokens assuming it follows the IWETH9 interface.
208
+ // Otherwise, it will revert.
209
+ IWETH9(token).deposit{ value: msg.value }();
210
+
211
+ // Cast `msg.value` to `uint128`.
212
+ uint128 amount = msg.value.toUint128();
213
+
214
+ // Enter the vault.
215
+ _enter({ vaultId: vaultId, from: address(this), amount: amount, token: IERC20(token) });
216
+ }
217
+
218
+ /// @inheritdoc ISablierBob
219
+ function redeem(uint256 vaultId)
220
+ external
221
+ payable
222
+ override
223
+ nonReentrant
224
+ notNull(vaultId)
225
+ returns (uint128 transferAmount, uint128 feeAmountDeductedFromYield)
226
+ {
227
+ // If the vault is active, sync the price from the oracle to update the status.
228
+ if (_statusOf(vaultId) == Bob.Status.ACTIVE) {
229
+ // Effect: sync the price from oracle.
230
+ _syncPriceFromOracle(vaultId);
231
+
232
+ // If it's still active after the sync, revert.
233
+ if (_statusOf(vaultId) == Bob.Status.ACTIVE) {
234
+ revert Errors.SablierBob_VaultStillActive(vaultId);
235
+ }
236
+
237
+ // Otherwise, the vault has been settled.
238
+ }
239
+
240
+ // Cache storage variables.
241
+ ISablierBobAdapter adapter = _vaults[vaultId].adapter;
242
+ IBobVaultShare shareToken = _vaults[vaultId].shareToken;
243
+ IERC20 token = _vaults[vaultId].token;
244
+
245
+ // Get the caller's share balance.
246
+ uint128 shareBalance = shareToken.balanceOf(msg.sender).toUint128();
247
+
248
+ // Check: the share balance is not zero.
249
+ if (shareBalance == 0) {
250
+ revert Errors.SablierBob_NoSharesToRedeem(vaultId, msg.sender);
251
+ }
252
+
253
+ // Check if the vault has an adapter.
254
+ if (address(adapter) != address(0)) {
255
+ // Check: the `msg.value` is zero.
256
+ if (msg.value > 0) {
257
+ revert Errors.SablierBob_MsgValueNotZero(vaultId);
258
+ }
259
+
260
+ // Check: the deposit token is staked with the adapter.
261
+ if (_vaults[vaultId].isStakedInAdapter) {
262
+ // Effect: set isStakedInAdapter to false.
263
+ _vaults[vaultId].isStakedInAdapter = false;
264
+
265
+ // Interaction: unstake all tokens via the adapter.
266
+ _unstakeFullAmountViaAdapter(vaultId, adapter);
267
+ }
268
+
269
+ // Interaction: Get the transfer amount and the fee deducted from yield.
270
+ (transferAmount, feeAmountDeductedFromYield) = adapter.processRedemption(vaultId, msg.sender, shareBalance);
271
+
272
+ // Interaction: transfer the fee to the comptroller address.
273
+ if (feeAmountDeductedFromYield > 0) {
274
+ token.safeTransfer(address(comptroller), feeAmountDeductedFromYield);
275
+ }
276
+ }
277
+ // Otherwise, check that `msg.value` is greater than or equal to the minimum fee required.
278
+ else {
279
+ // Get the minimum fee from the comptroller.
280
+ ISablierComptroller _comptroller = comptroller;
281
+ uint256 minFeeWei = _comptroller.calculateMinFeeWei({ protocol: ISablierComptroller.Protocol.Bob });
282
+
283
+ // Check: `msg.value` is greater than or equal to the minimum fee.
284
+ if (msg.value < minFeeWei) {
285
+ revert Errors.SablierBob_InsufficientFeePayment(msg.value, minFeeWei);
286
+ }
287
+
288
+ // Interaction: forward native token fee to comptroller.
289
+ if (msg.value > 0) {
290
+ (bool success,) = address(_comptroller).call{ value: msg.value }("");
291
+ if (!success) {
292
+ revert Errors.SablierBob_NativeFeeTransferFailed();
293
+ }
294
+ }
295
+
296
+ // Return the transfer amount.
297
+ transferAmount = shareBalance;
298
+ }
299
+
300
+ // Interaction: burn share tokens from the caller.
301
+ shareToken.burn(vaultId, msg.sender, shareBalance);
302
+
303
+ // Interaction: transfer tokens to the caller.
304
+ token.safeTransfer(msg.sender, transferAmount);
305
+
306
+ // Log the event.
307
+ emit Redeem({
308
+ vaultId: vaultId,
309
+ user: msg.sender,
310
+ amountReceived: transferAmount,
311
+ sharesBurned: shareBalance,
312
+ fee: feeAmountDeductedFromYield
313
+ });
314
+ }
315
+
316
+ /// @inheritdoc ISablierBob
317
+ function setNativeToken(address newNativeToken) external override onlyComptroller {
318
+ // Check: provided token is not zero address.
319
+ if (newNativeToken == address(0)) {
320
+ revert Errors.SablierBob_NativeTokenZeroAddress();
321
+ }
322
+
323
+ // Check: native token is not set.
324
+ if (nativeToken != address(0)) {
325
+ revert Errors.SablierBob_NativeTokenAlreadySet(nativeToken);
326
+ }
327
+
328
+ // Effect: set the native token.
329
+ nativeToken = newNativeToken;
330
+
331
+ // Log the update.
332
+ emit SetNativeToken({ comptroller: msg.sender, nativeToken: newNativeToken });
333
+ }
334
+
335
+ /// @inheritdoc ISablierBob
336
+ function setDefaultAdapter(IERC20 token, ISablierBobAdapter newAdapter) external override onlyComptroller {
337
+ // Check: the new adapter implements the {ISablierBobAdapter} interface.
338
+ if (address(newAdapter) != address(0)) {
339
+ bytes4 interfaceId = type(ISablierBobAdapter).interfaceId;
340
+ if (!IERC165(address(newAdapter)).supportsInterface(interfaceId)) {
341
+ revert Errors.SablierBob_NewAdapterMissesInterface(address(newAdapter));
342
+ }
343
+ }
344
+
345
+ // Effect: set the default adapter for the token.
346
+ _defaultAdapters[token] = newAdapter;
347
+
348
+ // Log the adapter change.
349
+ emit SetDefaultAdapter(token, newAdapter);
350
+ }
351
+
352
+ /// @inheritdoc ISablierBob
353
+ function syncPriceFromOracle(uint256 vaultId)
354
+ external
355
+ override
356
+ nonReentrant
357
+ notNull(vaultId)
358
+ onlyActive(vaultId)
359
+ returns (uint128 latestPrice)
360
+ {
361
+ // Effect: sync the price from oracle.
362
+ latestPrice = _syncPriceFromOracle(vaultId);
363
+ }
364
+
365
+ /// @inheritdoc ISablierBob
366
+ function unstakeTokensViaAdapter(uint256 vaultId)
367
+ external
368
+ override
369
+ nonReentrant
370
+ notNull(vaultId)
371
+ returns (uint128 amountReceivedFromAdapter)
372
+ {
373
+ // Cache storage variable.
374
+ ISablierBobAdapter adapter = _vaults[vaultId].adapter;
375
+
376
+ // Check: the vault has an adapter.
377
+ if (address(adapter) == address(0)) {
378
+ revert Errors.SablierBob_VaultHasNoAdapter(vaultId);
379
+ }
380
+
381
+ // Check: the vault has not already been unstaked.
382
+ if (!_vaults[vaultId].isStakedInAdapter) {
383
+ revert Errors.SablierBob_VaultAlreadyUnstaked(vaultId);
384
+ }
385
+
386
+ // Check: there is something to unstake.
387
+ if (adapter.getTotalYieldBearingTokenBalance(vaultId) == 0) {
388
+ revert Errors.SablierBob_UnstakeAmountZero(vaultId);
389
+ }
390
+
391
+ // If the vault is active, sync the price from the oracle to update the status.
392
+ if (_statusOf(vaultId) == Bob.Status.ACTIVE) {
393
+ // Effect: sync the price from oracle.
394
+ _syncPriceFromOracle(vaultId);
395
+
396
+ // If it's still active after the sync, revert.
397
+ if (_statusOf(vaultId) == Bob.Status.ACTIVE) {
398
+ revert Errors.SablierBob_VaultStillActive(vaultId);
399
+ }
400
+
401
+ // Otherwise, the vault has been settled.
402
+ }
403
+
404
+ // Effect: mark the vault as not staked with the adapter.
405
+ _vaults[vaultId].isStakedInAdapter = false;
406
+
407
+ // Interaction: unstake all tokens via the adapter.
408
+ amountReceivedFromAdapter = _unstakeFullAmountViaAdapter(vaultId, adapter);
409
+ }
410
+
411
+ /// @inheritdoc ISablierBob
412
+ function onShareTransfer(
413
+ uint256 vaultId,
414
+ address from,
415
+ address to,
416
+ uint256 amount,
417
+ uint256 fromBalanceBefore
418
+ )
419
+ external
420
+ override
421
+ {
422
+ // Check: caller is the share token for this vault.
423
+ if (msg.sender != address(_vaults[vaultId].shareToken)) {
424
+ revert Errors.SablierBob_CallerNotShareToken(vaultId, msg.sender);
425
+ }
426
+
427
+ // Cache storage variable.
428
+ ISablierBobAdapter adapter = _vaults[vaultId].adapter;
429
+ if (address(adapter) != address(0)) {
430
+ // Interaction: update staked token holding of the user in the adapter.
431
+ adapter.updateStakedTokenBalance(vaultId, from, to, amount, fromBalanceBefore);
432
+ }
433
+ }
434
+
435
+ /*//////////////////////////////////////////////////////////////////////////
436
+ PRIVATE STATE-CHANGING FUNCTIONS
437
+ //////////////////////////////////////////////////////////////////////////*/
438
+
439
+ /// @dev Common function to enter into a vault by depositing tokens into it and minting share tokens to caller.
440
+ /// @param vaultId The ID of the vault to deposit into.
441
+ /// @param from The address holding the vault token when calling this function. In case of native token deposits,
442
+ /// the vault tokens are held by this contract.
443
+ /// @param amount The amount of tokens to deposit.
444
+ /// @param token The ERC-20 token accepted by the vault.
445
+ function _enter(uint256 vaultId, address from, uint128 amount, IERC20 token) private {
446
+ // Check: the deposit amount is not zero.
447
+ if (amount == 0) {
448
+ revert Errors.SablierBob_DepositAmountZero(vaultId, msg.sender);
449
+ }
450
+
451
+ // Effect: sync the price from oracle.
452
+ _syncPriceFromOracle(vaultId);
453
+
454
+ // Check: the vault is still active after the price sync.
455
+ _revertIfSettledOrExpired(vaultId);
456
+
457
+ // Cache storage variables.
458
+ ISablierBobAdapter adapter = _vaults[vaultId].adapter;
459
+
460
+ // If adapter is set, transfer tokens to the adapter.
461
+ if (address(adapter) != address(0)) {
462
+ // Interaction: transfer tokens to the adapter. Use `safeTransfer` for the native token path since
463
+ // the contract already holds the wrapped tokens.
464
+ if (from == address(this)) {
465
+ token.safeTransfer(address(adapter), amount);
466
+ } else {
467
+ token.safeTransferFrom(from, address(adapter), amount);
468
+ }
469
+
470
+ // Interaction: stake tokens via the adapter on behalf of the caller.
471
+ adapter.stake(vaultId, msg.sender, amount);
472
+ }
473
+ // Otherwise, if `from` is `msg.sender`, transfer tokens to this contract. When this function is called by
474
+ // `enterWithNativeToken`, the vault tokens are held by this contract already.
475
+ else if (from == msg.sender) {
476
+ // Interaction: transfer tokens from caller to this contract.
477
+ token.safeTransferFrom(from, address(this), amount);
478
+ }
479
+
480
+ // Interaction: mint share tokens to the caller.
481
+ _vaults[vaultId].shareToken.mint(vaultId, msg.sender, amount);
482
+
483
+ // Log the deposit.
484
+ emit Enter({ vaultId: vaultId, user: msg.sender, amountReceived: amount, sharesMinted: amount });
485
+ }
486
+
487
+ /// @notice Private function that reverts if the vault is settled or expired.
488
+ /// @param vaultId The ID of the vault.
489
+ function _revertIfSettledOrExpired(uint256 vaultId) private view {
490
+ if (_statusOf(vaultId) != Bob.Status.ACTIVE) {
491
+ revert Errors.SablierBob_VaultNotActive(vaultId);
492
+ }
493
+ }
494
+
495
+ /// @dev Private function to fetch the latest oracle price and update it in the vault storage.
496
+ /// @param vaultId The ID of the vault.
497
+ /// @return latestPrice The latest price from the oracle.
498
+ function _syncPriceFromOracle(uint256 vaultId) private returns (uint128 latestPrice) {
499
+ AggregatorV3Interface oracleAddress = _vaults[vaultId].oracle;
500
+
501
+ // Get the latest price, normalized to 8 decimals, from the oracle with safety checks.
502
+ (latestPrice,,) = SafeOracle.safeOraclePrice({ oracle: oracleAddress, normalize: true });
503
+
504
+ // Return if the latest price is zero.
505
+ if (latestPrice == 0) {
506
+ return 0;
507
+ }
508
+
509
+ uint40 currentTimestamp = uint40(block.timestamp);
510
+
511
+ // Effect: update the last synced price and timestamp.
512
+ _vaults[vaultId].lastSyncedPrice = latestPrice;
513
+ _vaults[vaultId].lastSyncedAt = currentTimestamp;
514
+
515
+ // Log the event.
516
+ emit SyncPriceFromOracle({
517
+ vaultId: vaultId,
518
+ oracle: oracleAddress,
519
+ latestPrice: latestPrice,
520
+ syncedAt: currentTimestamp
521
+ });
522
+ }
523
+
524
+ /// @dev Private function to unstake all tokens using the adapter.
525
+ /// @param vaultId The ID of the vault.
526
+ /// @param adapter The adapter to use for unstaking.
527
+ /// @return amountReceivedFromAdapter The amount of tokens received from the adapter after unstaking.
528
+ function _unstakeFullAmountViaAdapter(
529
+ uint256 vaultId,
530
+ ISablierBobAdapter adapter
531
+ )
532
+ private
533
+ returns (uint128 amountReceivedFromAdapter)
534
+ {
535
+ uint128 wrappedTokenUnstakedAmount;
536
+
537
+ // Interaction: unstake all tokens via the adapter.
538
+ (wrappedTokenUnstakedAmount, amountReceivedFromAdapter) = adapter.unstakeFullAmount(vaultId);
539
+
540
+ // Log the event.
541
+ emit UnstakeFromAdapter(vaultId, adapter, wrappedTokenUnstakedAmount, amountReceivedFromAdapter);
542
+ }
543
+ }