@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.
- package/CHANGELOG.md +19 -0
- package/LICENSE-BUSL.md +82 -0
- package/LICENSE-GPL.md +470 -0
- package/LICENSE.md +9 -0
- package/README.md +81 -0
- package/artifacts/BobVaultShare.json +936 -0
- package/artifacts/SablierBob.json +1683 -0
- package/artifacts/SablierEscrow.json +1316 -0
- package/artifacts/SablierLidoAdapter.json +1649 -0
- package/artifacts/erc20/IERC20.json +226 -0
- package/artifacts/interfaces/IBobVaultShare.json +393 -0
- package/artifacts/interfaces/ISablierBob.json +1171 -0
- package/artifacts/interfaces/ISablierEscrow.json +999 -0
- package/artifacts/interfaces/ISablierLidoAdapter.json +1141 -0
- package/artifacts/interfaces/external/ICurveStETHPool.json +128 -0
- package/artifacts/interfaces/external/ILidoWithdrawalQueue.json +209 -0
- package/artifacts/interfaces/external/IStETH.json +262 -0
- package/artifacts/interfaces/external/IWETH9.json +259 -0
- package/artifacts/interfaces/external/IWstETH.json +311 -0
- package/artifacts/libraries/Errors.json +868 -0
- package/package.json +68 -0
- package/src/BobVaultShare.sol +119 -0
- package/src/SablierBob.sol +543 -0
- package/src/SablierEscrow.sol +288 -0
- package/src/SablierLidoAdapter.sol +549 -0
- package/src/abstracts/SablierBobState.sol +156 -0
- package/src/abstracts/SablierEscrowState.sol +159 -0
- package/src/interfaces/IBobVaultShare.sol +51 -0
- package/src/interfaces/ISablierBob.sol +261 -0
- package/src/interfaces/ISablierBobAdapter.sol +157 -0
- package/src/interfaces/ISablierBobState.sol +74 -0
- package/src/interfaces/ISablierEscrow.sol +148 -0
- package/src/interfaces/ISablierEscrowState.sol +77 -0
- package/src/interfaces/ISablierLidoAdapter.sol +110 -0
- package/src/interfaces/external/ICurveStETHPool.sol +31 -0
- package/src/interfaces/external/ILidoWithdrawalQueue.sol +67 -0
- package/src/interfaces/external/IStETH.sol +18 -0
- package/src/interfaces/external/IWETH9.sol +19 -0
- package/src/interfaces/external/IWstETH.sol +32 -0
- package/src/libraries/Errors.sol +189 -0
- package/src/types/Bob.sol +49 -0
- package/src/types/Escrow.sol +49 -0
|
@@ -0,0 +1,549 @@
|
|
|
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 { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
6
|
+
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
7
|
+
import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
8
|
+
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
9
|
+
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
|
|
10
|
+
import { ud, UD60x18, UNIT } from "@prb/math/src/UD60x18.sol";
|
|
11
|
+
import { Comptrollerable } from "@sablier/evm-utils/src/Comptrollerable.sol";
|
|
12
|
+
import { SafeOracle } from "@sablier/evm-utils/src/libraries/SafeOracle.sol";
|
|
13
|
+
|
|
14
|
+
import { ICurveStETHPool } from "./interfaces/external/ICurveStETHPool.sol";
|
|
15
|
+
import { ILidoWithdrawalQueue } from "./interfaces/external/ILidoWithdrawalQueue.sol";
|
|
16
|
+
import { IStETH } from "./interfaces/external/IStETH.sol";
|
|
17
|
+
import { IWETH9 } from "./interfaces/external/IWETH9.sol";
|
|
18
|
+
import { IWstETH } from "./interfaces/external/IWstETH.sol";
|
|
19
|
+
import { ISablierBobAdapter } from "./interfaces/ISablierBobAdapter.sol";
|
|
20
|
+
import { ISablierBobState } from "./interfaces/ISablierBobState.sol";
|
|
21
|
+
import { ISablierLidoAdapter } from "./interfaces/ISablierLidoAdapter.sol";
|
|
22
|
+
import { Errors } from "./libraries/Errors.sol";
|
|
23
|
+
import { Bob } from "./types/Bob.sol";
|
|
24
|
+
|
|
25
|
+
/// @title SablierLidoAdapter
|
|
26
|
+
/// @notice Lido yield adapter for the SablierBob protocol.
|
|
27
|
+
/// @dev This adapter stakes WETH as wstETH to earn Lido staking rewards.
|
|
28
|
+
contract SablierLidoAdapter is
|
|
29
|
+
Comptrollerable, // 1 inherited component
|
|
30
|
+
ERC165, // 1 inherited component
|
|
31
|
+
ISablierLidoAdapter // 2 inherited components
|
|
32
|
+
{
|
|
33
|
+
using SafeERC20 for IERC20;
|
|
34
|
+
using SafeCast for uint256;
|
|
35
|
+
|
|
36
|
+
/*//////////////////////////////////////////////////////////////////////////
|
|
37
|
+
CONSTANTS
|
|
38
|
+
//////////////////////////////////////////////////////////////////////////*/
|
|
39
|
+
|
|
40
|
+
/// @inheritdoc ISablierLidoAdapter
|
|
41
|
+
address public immutable override CURVE_POOL;
|
|
42
|
+
|
|
43
|
+
/// @inheritdoc ISablierLidoAdapter
|
|
44
|
+
address public immutable override LIDO_WITHDRAWAL_QUEUE;
|
|
45
|
+
|
|
46
|
+
/// @inheritdoc ISablierBobAdapter
|
|
47
|
+
UD60x18 public constant override MAX_FEE = UD60x18.wrap(0.2e18);
|
|
48
|
+
|
|
49
|
+
/// @inheritdoc ISablierLidoAdapter
|
|
50
|
+
UD60x18 public constant override MAX_SLIPPAGE_TOLERANCE = UD60x18.wrap(0.05e18);
|
|
51
|
+
|
|
52
|
+
/// @inheritdoc ISablierLidoAdapter
|
|
53
|
+
address public immutable override STETH;
|
|
54
|
+
|
|
55
|
+
/// @inheritdoc ISablierLidoAdapter
|
|
56
|
+
address public immutable override STETH_ETH_ORACLE;
|
|
57
|
+
|
|
58
|
+
/// @inheritdoc ISablierLidoAdapter
|
|
59
|
+
address public immutable override WETH;
|
|
60
|
+
|
|
61
|
+
/// @inheritdoc ISablierLidoAdapter
|
|
62
|
+
address public immutable override WSTETH;
|
|
63
|
+
|
|
64
|
+
/// @inheritdoc ISablierBobAdapter
|
|
65
|
+
address public immutable override SABLIER_BOB;
|
|
66
|
+
|
|
67
|
+
/*//////////////////////////////////////////////////////////////////////////
|
|
68
|
+
PUBLIC STORAGE
|
|
69
|
+
//////////////////////////////////////////////////////////////////////////*/
|
|
70
|
+
|
|
71
|
+
/// @inheritdoc ISablierBobAdapter
|
|
72
|
+
UD60x18 public override feeOnYield;
|
|
73
|
+
|
|
74
|
+
/// @inheritdoc ISablierLidoAdapter
|
|
75
|
+
UD60x18 public override slippageTolerance;
|
|
76
|
+
|
|
77
|
+
/*//////////////////////////////////////////////////////////////////////////
|
|
78
|
+
INTERNAL STORAGE
|
|
79
|
+
//////////////////////////////////////////////////////////////////////////*/
|
|
80
|
+
|
|
81
|
+
/// @dev Lido withdrawal request IDs for each vault.
|
|
82
|
+
mapping(uint256 vaultId => uint256[] requestIds) internal _lidoWithdrawalRequestIds;
|
|
83
|
+
|
|
84
|
+
/// @dev wstETH amount held for each user in each vault.
|
|
85
|
+
mapping(uint256 vaultId => mapping(address user => uint128 wstETHAmount)) internal _userWstETH;
|
|
86
|
+
|
|
87
|
+
/// @dev Total wstETH amount held in each vault.
|
|
88
|
+
mapping(uint256 vaultId => uint128 totalWstETH) internal _vaultTotalWstETH;
|
|
89
|
+
|
|
90
|
+
/// @dev Yield fee snapshotted for each vault at creation time.
|
|
91
|
+
mapping(uint256 vaultId => UD60x18 fee) internal _vaultYieldFee;
|
|
92
|
+
|
|
93
|
+
/// @dev Total WETH received after unstaking all tokens in a vault.
|
|
94
|
+
mapping(uint256 vaultId => uint128 wethReceived) internal _wethReceivedAfterUnstaking;
|
|
95
|
+
|
|
96
|
+
/*//////////////////////////////////////////////////////////////////////////
|
|
97
|
+
CONSTRUCTOR
|
|
98
|
+
//////////////////////////////////////////////////////////////////////////*/
|
|
99
|
+
|
|
100
|
+
/// @notice Deploys the Lido adapter.
|
|
101
|
+
/// @param initialComptroller The address of the initial comptroller contract.
|
|
102
|
+
/// @param sablierBob The address of the SablierBob contract.
|
|
103
|
+
/// @param curvePool The address of the Curve stETH/ETH pool.
|
|
104
|
+
/// @param lidoWithdrawalQueue The address of the Lido withdrawal queue contract.
|
|
105
|
+
/// @param steth The address of the stETH contract.
|
|
106
|
+
/// @param stethEthOracle The address of the Chainlink's stETH/ETH oracle.
|
|
107
|
+
/// @param weth The address of the WETH contract.
|
|
108
|
+
/// @param wsteth The address of the wstETH contract.
|
|
109
|
+
/// @param initialSlippageTolerance The initial slippage tolerance for Curve swaps as UD60x18.
|
|
110
|
+
/// @param initialYieldFee The initial yield fee as UD60x18.
|
|
111
|
+
constructor(
|
|
112
|
+
address initialComptroller,
|
|
113
|
+
address sablierBob,
|
|
114
|
+
address curvePool,
|
|
115
|
+
address lidoWithdrawalQueue,
|
|
116
|
+
address steth,
|
|
117
|
+
address stethEthOracle,
|
|
118
|
+
address weth,
|
|
119
|
+
address wsteth,
|
|
120
|
+
UD60x18 initialSlippageTolerance,
|
|
121
|
+
UD60x18 initialYieldFee
|
|
122
|
+
)
|
|
123
|
+
Comptrollerable(initialComptroller)
|
|
124
|
+
{
|
|
125
|
+
// Check: the slippage tolerance is not too high.
|
|
126
|
+
if (initialSlippageTolerance.gt(MAX_SLIPPAGE_TOLERANCE)) {
|
|
127
|
+
revert Errors.SablierLidoAdapter_SlippageToleranceTooHigh(initialSlippageTolerance, MAX_SLIPPAGE_TOLERANCE);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check: the yield fee is not too high.
|
|
131
|
+
if (initialYieldFee.gt(MAX_FEE)) {
|
|
132
|
+
revert Errors.SablierLidoAdapter_YieldFeeTooHigh(initialYieldFee, MAX_FEE);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
SABLIER_BOB = sablierBob;
|
|
136
|
+
CURVE_POOL = curvePool;
|
|
137
|
+
LIDO_WITHDRAWAL_QUEUE = lidoWithdrawalQueue;
|
|
138
|
+
STETH = steth;
|
|
139
|
+
STETH_ETH_ORACLE = stethEthOracle;
|
|
140
|
+
WETH = weth;
|
|
141
|
+
WSTETH = wsteth;
|
|
142
|
+
|
|
143
|
+
// Effect: set the initial slippage tolerance.
|
|
144
|
+
slippageTolerance = initialSlippageTolerance;
|
|
145
|
+
|
|
146
|
+
// Effect: set the initial yield fee.
|
|
147
|
+
feeOnYield = initialYieldFee;
|
|
148
|
+
|
|
149
|
+
// Approve wstETH contract to spend stETH, required for wrapping.
|
|
150
|
+
IStETH(STETH).approve(WSTETH, type(uint128).max);
|
|
151
|
+
|
|
152
|
+
// Approve Curve pool to spend stETH, required for unwrapping.
|
|
153
|
+
IStETH(STETH).approve(CURVE_POOL, type(uint128).max);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/*//////////////////////////////////////////////////////////////////////////
|
|
157
|
+
MODIFIERS
|
|
158
|
+
//////////////////////////////////////////////////////////////////////////*/
|
|
159
|
+
|
|
160
|
+
/// @dev Reverts if the caller is not SablierBob.
|
|
161
|
+
modifier onlySablierBob() {
|
|
162
|
+
if (msg.sender != SABLIER_BOB) {
|
|
163
|
+
revert Errors.SablierLidoAdapter_OnlySablierBob(msg.sender, SABLIER_BOB);
|
|
164
|
+
}
|
|
165
|
+
_;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/*//////////////////////////////////////////////////////////////////////////
|
|
169
|
+
USER-FACING READ-ONLY FUNCTIONS
|
|
170
|
+
//////////////////////////////////////////////////////////////////////////*/
|
|
171
|
+
|
|
172
|
+
/// @inheritdoc ISablierLidoAdapter
|
|
173
|
+
function getLidoWithdrawalRequestIds(uint256 vaultId) external view override returns (uint256[] memory) {
|
|
174
|
+
return _lidoWithdrawalRequestIds[vaultId];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// @inheritdoc ISablierBobAdapter
|
|
178
|
+
function getTotalYieldBearingTokenBalance(uint256 vaultId) external view override returns (uint128) {
|
|
179
|
+
return _vaultTotalWstETH[vaultId];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// @inheritdoc ISablierBobAdapter
|
|
183
|
+
function getVaultYieldFee(uint256 vaultId) external view override returns (UD60x18) {
|
|
184
|
+
return _vaultYieldFee[vaultId];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// @inheritdoc ISablierLidoAdapter
|
|
188
|
+
function getWethReceivedAfterUnstaking(uint256 vaultId) external view override returns (uint256) {
|
|
189
|
+
return _wethReceivedAfterUnstaking[vaultId];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// @inheritdoc ISablierBobAdapter
|
|
193
|
+
function getYieldBearingTokenBalanceFor(uint256 vaultId, address user) external view override returns (uint128) {
|
|
194
|
+
return _userWstETH[vaultId][user];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// @inheritdoc ERC165
|
|
198
|
+
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
199
|
+
return interfaceId == type(ISablierBobAdapter).interfaceId
|
|
200
|
+
|| interfaceId == type(ISablierLidoAdapter).interfaceId || super.supportsInterface(interfaceId);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/*//////////////////////////////////////////////////////////////////////////
|
|
204
|
+
USER-FACING STATE-CHANGING FUNCTIONS
|
|
205
|
+
//////////////////////////////////////////////////////////////////////////*/
|
|
206
|
+
|
|
207
|
+
/// @inheritdoc ISablierBobAdapter
|
|
208
|
+
function processRedemption(
|
|
209
|
+
uint256 vaultId,
|
|
210
|
+
address user,
|
|
211
|
+
uint128 shareBalance
|
|
212
|
+
)
|
|
213
|
+
external
|
|
214
|
+
override
|
|
215
|
+
onlySablierBob
|
|
216
|
+
returns (uint128 transferAmount, uint128 feeAmountDeductedFromYield)
|
|
217
|
+
{
|
|
218
|
+
// Get total amount of wstETH in the vault before unstaking.
|
|
219
|
+
uint256 totalWstETH = _vaultTotalWstETH[vaultId];
|
|
220
|
+
|
|
221
|
+
// Get total amount of WETH received after unstaking all tokens in the vault.
|
|
222
|
+
uint256 totalWeth = _wethReceivedAfterUnstaking[vaultId];
|
|
223
|
+
|
|
224
|
+
// If total wstETH or total WETH received is zero, return zero.
|
|
225
|
+
if (totalWstETH == 0 || totalWeth == 0) {
|
|
226
|
+
return (0, 0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Get wstETH allocated to the user before unstaking.
|
|
230
|
+
uint256 userWstETH = _userWstETH[vaultId][user];
|
|
231
|
+
|
|
232
|
+
// Calculate user's proportional share of WETH.
|
|
233
|
+
uint128 userWethShare = (userWstETH * totalWeth / totalWstETH).toUint128();
|
|
234
|
+
|
|
235
|
+
// If the user's share of WETH is greater than the user's vault share, the yield is positive and we need to
|
|
236
|
+
// calculate the fee.
|
|
237
|
+
if (userWethShare > shareBalance) {
|
|
238
|
+
uint128 yieldAmount = userWethShare - shareBalance;
|
|
239
|
+
|
|
240
|
+
// Calculate the fee.
|
|
241
|
+
feeAmountDeductedFromYield = ud(yieldAmount).mul(_vaultYieldFee[vaultId]).intoUint128();
|
|
242
|
+
transferAmount = userWethShare - feeAmountDeductedFromYield;
|
|
243
|
+
}
|
|
244
|
+
// Otherwise, the yield is negative or zero, so no fee is applicable.
|
|
245
|
+
else {
|
|
246
|
+
transferAmount = userWethShare;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Effect: clear the user's wstETH balance.
|
|
250
|
+
delete _userWstETH[vaultId][user];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// @inheritdoc ISablierBobAdapter
|
|
254
|
+
function registerVault(uint256 vaultId) external override onlySablierBob {
|
|
255
|
+
// Effect: snapshot the current global yield fee for this vault.
|
|
256
|
+
_vaultYieldFee[vaultId] = feeOnYield;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// @inheritdoc ISablierLidoAdapter
|
|
260
|
+
function requestLidoWithdrawal(uint256 vaultId) external override onlyComptroller {
|
|
261
|
+
// Check: the vault status is not ACTIVE.
|
|
262
|
+
if (ISablierBobState(SABLIER_BOB).statusOf(vaultId) == Bob.Status.ACTIVE) {
|
|
263
|
+
revert Errors.SablierLidoAdapter_VaultActive(vaultId);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check: the vault tokens are still staked in the adapter.
|
|
267
|
+
if (!ISablierBobState(SABLIER_BOB).isStakedInAdapter(vaultId)) {
|
|
268
|
+
revert Errors.SablierLidoAdapter_VaultAlreadyUnstaked(vaultId);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check: Lido withdrawal has not already been requested for this vault.
|
|
272
|
+
if (_lidoWithdrawalRequestIds[vaultId].length > 0) {
|
|
273
|
+
revert Errors.SablierLidoAdapter_LidoWithdrawalAlreadyRequested(vaultId);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Get total wstETH in the vault.
|
|
277
|
+
uint128 totalWstETH = _vaultTotalWstETH[vaultId];
|
|
278
|
+
|
|
279
|
+
// Check: total wstETH is not zero.
|
|
280
|
+
if (totalWstETH == 0) {
|
|
281
|
+
revert Errors.SablierLidoAdapter_NoWstETHToWithdraw(vaultId);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Interaction: Unwrap wstETH to get stETH.
|
|
285
|
+
uint256 stETHAmount = IWstETH(WSTETH).unwrap(totalWstETH);
|
|
286
|
+
|
|
287
|
+
// Get the maximum and minimum amounts that can be withdrawn in a single request.
|
|
288
|
+
uint256 maxAmountPerRequest = ILidoWithdrawalQueue(LIDO_WITHDRAWAL_QUEUE).MAX_STETH_WITHDRAWAL_AMOUNT();
|
|
289
|
+
uint256 minAmountPerRequest = ILidoWithdrawalQueue(LIDO_WITHDRAWAL_QUEUE).MIN_STETH_WITHDRAWAL_AMOUNT();
|
|
290
|
+
|
|
291
|
+
// Check: the total amount to withdraw is not less than the minimum amount per request.
|
|
292
|
+
if (stETHAmount < minAmountPerRequest) {
|
|
293
|
+
revert Errors.SablierLidoAdapter_WithdrawalAmountBelowMinimum(vaultId, stETHAmount, minAmountPerRequest);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Declare amounts array.
|
|
297
|
+
uint256[] memory amounts;
|
|
298
|
+
|
|
299
|
+
// If the total amount to be withdrawn is greater than the maximum amount per request, split it into multiple
|
|
300
|
+
// requests.
|
|
301
|
+
if (stETHAmount > maxAmountPerRequest) {
|
|
302
|
+
// Calculate the total number of requests required to withdraw the full amount using the ceiling division.
|
|
303
|
+
uint256 totalRequests = (stETHAmount + maxAmountPerRequest - 1) / maxAmountPerRequest;
|
|
304
|
+
|
|
305
|
+
// Initialize array length to the total number of requests.
|
|
306
|
+
amounts = new uint256[](totalRequests);
|
|
307
|
+
|
|
308
|
+
// Assign amounts for each request except the last one.
|
|
309
|
+
uint256 lastIndex = totalRequests - 1;
|
|
310
|
+
for (uint256 i; i < lastIndex; ++i) {
|
|
311
|
+
amounts[i] = maxAmountPerRequest;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Assign the remaining amount to the last request.
|
|
315
|
+
uint256 remainder = stETHAmount - maxAmountPerRequest * lastIndex;
|
|
316
|
+
|
|
317
|
+
// If the remainder is below the minimum amount per request, deduct the difference from the second last
|
|
318
|
+
// request.
|
|
319
|
+
if (remainder < minAmountPerRequest) {
|
|
320
|
+
// Calculate the difference.
|
|
321
|
+
uint256 diff = minAmountPerRequest - remainder;
|
|
322
|
+
|
|
323
|
+
// Deduct the difference from the second last request.
|
|
324
|
+
amounts[lastIndex - 1] -= diff;
|
|
325
|
+
|
|
326
|
+
// Set the remainder to the minimum amount per request.
|
|
327
|
+
remainder = minAmountPerRequest;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
amounts[lastIndex] = remainder;
|
|
331
|
+
}
|
|
332
|
+
// Otherwise, its just one request.
|
|
333
|
+
else {
|
|
334
|
+
// Initialize array length to 1.
|
|
335
|
+
amounts = new uint256[](1);
|
|
336
|
+
amounts[0] = stETHAmount;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Interaction: Approve Lido withdrawal queue to spend the exact stETH amount.
|
|
340
|
+
IStETH(STETH).approve(LIDO_WITHDRAWAL_QUEUE, stETHAmount);
|
|
341
|
+
|
|
342
|
+
// Interaction: Submit stETH to Lido's withdrawal queue.
|
|
343
|
+
uint256[] memory requestIds =
|
|
344
|
+
ILidoWithdrawalQueue(LIDO_WITHDRAWAL_QUEUE).requestWithdrawals(amounts, address(this));
|
|
345
|
+
|
|
346
|
+
// Effect: store request IDs for later claiming (also disables the Curve path for this vault).
|
|
347
|
+
_lidoWithdrawalRequestIds[vaultId] = requestIds;
|
|
348
|
+
|
|
349
|
+
// Log the event.
|
|
350
|
+
emit RequestLidoWithdrawal(vaultId, msg.sender, totalWstETH, stETHAmount, requestIds);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// @inheritdoc ISablierLidoAdapter
|
|
354
|
+
function setSlippageTolerance(UD60x18 newTolerance) external override onlyComptroller {
|
|
355
|
+
// Check: the slippage tolerance does not exceed MAX_SLIPPAGE_TOLERANCE.
|
|
356
|
+
if (newTolerance.gt(MAX_SLIPPAGE_TOLERANCE)) {
|
|
357
|
+
revert Errors.SablierLidoAdapter_SlippageToleranceTooHigh(newTolerance, MAX_SLIPPAGE_TOLERANCE);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Cache the current slippage tolerance.
|
|
361
|
+
UD60x18 previousTolerance = slippageTolerance;
|
|
362
|
+
|
|
363
|
+
// Effect: set the new slippage tolerance.
|
|
364
|
+
slippageTolerance = newTolerance;
|
|
365
|
+
|
|
366
|
+
// Log the event.
|
|
367
|
+
emit SetSlippageTolerance(previousTolerance, newTolerance);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/// @inheritdoc ISablierBobAdapter
|
|
371
|
+
function setYieldFee(UD60x18 newFee) external override onlyComptroller {
|
|
372
|
+
// Check: the new fee does not exceed MAX_FEE.
|
|
373
|
+
if (newFee.gt(MAX_FEE)) {
|
|
374
|
+
revert Errors.SablierLidoAdapter_YieldFeeTooHigh(newFee, MAX_FEE);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
UD60x18 previousFee = feeOnYield;
|
|
378
|
+
|
|
379
|
+
// Effect: set the new fee.
|
|
380
|
+
feeOnYield = newFee;
|
|
381
|
+
|
|
382
|
+
// Log the event.
|
|
383
|
+
emit SetYieldFee(previousFee, newFee);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/// @inheritdoc ISablierBobAdapter
|
|
387
|
+
function stake(uint256 vaultId, address user, uint256 amount) external override onlySablierBob {
|
|
388
|
+
// Interaction: Unwrap WETH into ETH.
|
|
389
|
+
IWETH9(WETH).withdraw(amount);
|
|
390
|
+
|
|
391
|
+
// Interaction: Stake ETH to get stETH.
|
|
392
|
+
IStETH(STETH).submit{ value: amount }({ referral: address(comptroller) });
|
|
393
|
+
|
|
394
|
+
// Get the balance of stETH held by the adapter.
|
|
395
|
+
uint256 stETHBalance = IStETH(STETH).balanceOf(address(this));
|
|
396
|
+
|
|
397
|
+
// Interaction: Wrap stETH into wstETH.
|
|
398
|
+
uint128 wstETHAmount = IWstETH(WSTETH).wrap(stETHBalance).toUint128();
|
|
399
|
+
|
|
400
|
+
// Effect: track user's wstETH.
|
|
401
|
+
_userWstETH[vaultId][user] += wstETHAmount;
|
|
402
|
+
_vaultTotalWstETH[vaultId] += wstETHAmount;
|
|
403
|
+
|
|
404
|
+
// Log the event.
|
|
405
|
+
emit Stake(vaultId, user, amount, wstETHAmount);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/// @inheritdoc ISablierBobAdapter
|
|
409
|
+
function unstakeFullAmount(uint256 vaultId)
|
|
410
|
+
external
|
|
411
|
+
override
|
|
412
|
+
onlySablierBob
|
|
413
|
+
returns (uint128 totalWstETH, uint128 amountReceivedFromUnstaking)
|
|
414
|
+
{
|
|
415
|
+
// Get total amount of wstETH in the vault.
|
|
416
|
+
totalWstETH = _vaultTotalWstETH[vaultId];
|
|
417
|
+
|
|
418
|
+
// If a Lido withdrawal was requested, claim from the withdrawal queue.
|
|
419
|
+
if (_lidoWithdrawalRequestIds[vaultId].length > 0) {
|
|
420
|
+
amountReceivedFromUnstaking = _claimLidoWithdrawals(vaultId);
|
|
421
|
+
}
|
|
422
|
+
// Otherwise, swap via Curve.
|
|
423
|
+
else {
|
|
424
|
+
amountReceivedFromUnstaking = _swapWstETHToWeth(totalWstETH);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Effect: store the total WETH received for redemption calculations.
|
|
428
|
+
_wethReceivedAfterUnstaking[vaultId] = amountReceivedFromUnstaking;
|
|
429
|
+
|
|
430
|
+
// Interaction: transfer WETH to SablierBob for distribution.
|
|
431
|
+
IERC20(WETH).safeTransfer(SABLIER_BOB, amountReceivedFromUnstaking);
|
|
432
|
+
|
|
433
|
+
// Log the event.
|
|
434
|
+
emit UnstakeFullAmount({
|
|
435
|
+
vaultId: vaultId,
|
|
436
|
+
totalStakedAmount: totalWstETH,
|
|
437
|
+
amountReceivedFromUnstaking: amountReceivedFromUnstaking
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// @inheritdoc ISablierBobAdapter
|
|
442
|
+
function updateStakedTokenBalance(
|
|
443
|
+
uint256 vaultId,
|
|
444
|
+
address from,
|
|
445
|
+
address to,
|
|
446
|
+
uint256 shareAmountTransferred,
|
|
447
|
+
uint256 userShareBalanceBeforeTransfer
|
|
448
|
+
)
|
|
449
|
+
external
|
|
450
|
+
override
|
|
451
|
+
onlySablierBob
|
|
452
|
+
{
|
|
453
|
+
// Check: the user's balance is not zero.
|
|
454
|
+
if (userShareBalanceBeforeTransfer == 0) {
|
|
455
|
+
revert Errors.SablierLidoAdapter_UserBalanceZero(vaultId, from);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Calculate proportional wstETH to transfer.
|
|
459
|
+
uint256 fromWstETH = _userWstETH[vaultId][from];
|
|
460
|
+
|
|
461
|
+
// Calculate the portion of wstETH to transfer.
|
|
462
|
+
uint128 wstETHToTransfer = (fromWstETH * shareAmountTransferred / userShareBalanceBeforeTransfer).toUint128();
|
|
463
|
+
|
|
464
|
+
// Check: the wstETH transfer amount is not zero.
|
|
465
|
+
if (wstETHToTransfer == 0) {
|
|
466
|
+
revert Errors.SablierLidoAdapter_WstETHTransferAmountZero(vaultId, from, to);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Effect: move wstETH from sender to recipient.
|
|
470
|
+
_userWstETH[vaultId][from] -= wstETHToTransfer;
|
|
471
|
+
_userWstETH[vaultId][to] += wstETHToTransfer;
|
|
472
|
+
|
|
473
|
+
// Log the event.
|
|
474
|
+
emit TransferStakedTokens(vaultId, from, to, wstETHToTransfer);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/*//////////////////////////////////////////////////////////////////////////
|
|
478
|
+
PRIVATE STATE-CHANGING FUNCTIONS
|
|
479
|
+
//////////////////////////////////////////////////////////////////////////*/
|
|
480
|
+
|
|
481
|
+
/// @dev Claims finalized Lido withdrawals for a vault and wraps the received ETH into WETH.
|
|
482
|
+
function _claimLidoWithdrawals(uint256 vaultId) private returns (uint128 wethReceived) {
|
|
483
|
+
uint256[] memory requestIds = _lidoWithdrawalRequestIds[vaultId];
|
|
484
|
+
|
|
485
|
+
// Interaction: Since Lido processes withdrawals in batches, we need to find the number of total finalized
|
|
486
|
+
// batches occurred so far. This is used in the next step.
|
|
487
|
+
uint256 lastIndex = ILidoWithdrawalQueue(LIDO_WITHDRAWAL_QUEUE).getLastCheckpointIndex();
|
|
488
|
+
|
|
489
|
+
// Interaction: search the request IDs in all the finalized batches. If any request ID is not finalized, the
|
|
490
|
+
// corresponding hint will be zero.
|
|
491
|
+
uint256[] memory hints =
|
|
492
|
+
ILidoWithdrawalQueue(LIDO_WITHDRAWAL_QUEUE).findCheckpointHints(requestIds, 1, lastIndex);
|
|
493
|
+
|
|
494
|
+
// Get the ETH balance before claiming.
|
|
495
|
+
uint256 ethBefore = address(this).balance;
|
|
496
|
+
|
|
497
|
+
// Interaction: Claim all request IDs. It will revert if any request ID is not finalized.
|
|
498
|
+
ILidoWithdrawalQueue(LIDO_WITHDRAWAL_QUEUE).claimWithdrawals(requestIds, hints);
|
|
499
|
+
|
|
500
|
+
// Calculate the ETH received from claiming.
|
|
501
|
+
uint256 ethReceived = address(this).balance - ethBefore;
|
|
502
|
+
|
|
503
|
+
// Cast ethReceived to uint128.
|
|
504
|
+
wethReceived = ethReceived.toUint128();
|
|
505
|
+
|
|
506
|
+
// Interaction: Wrap ETH to get WETH.
|
|
507
|
+
IWETH9(WETH).deposit{ value: ethReceived }();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/// @dev Swap wstETH to WETH using Curve exchange, with oracle-based slippage protection.
|
|
511
|
+
function _swapWstETHToWeth(uint128 wstETHAmount) private returns (uint128 wethReceived) {
|
|
512
|
+
// Interaction: Unwrap wstETH to get stETH.
|
|
513
|
+
uint256 stETHAmount = IWstETH(WSTETH).unwrap(wstETHAmount);
|
|
514
|
+
|
|
515
|
+
// Get the stETH/ETH price from the Chainlink oracle in its native 18 decimals.
|
|
516
|
+
(uint128 oraclePrice,,) =
|
|
517
|
+
SafeOracle.safeOraclePrice({ oracle: AggregatorV3Interface(STETH_ETH_ORACLE), normalize: false });
|
|
518
|
+
|
|
519
|
+
// Check: the oracle price is not zero.
|
|
520
|
+
if (oraclePrice == 0) {
|
|
521
|
+
revert Errors.SablierLidoAdapter_OraclePriceZero();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Calculate the fair ETH output using the oracle price as a manipulation-resistant reference.
|
|
525
|
+
uint256 fairEthOut = stETHAmount * oraclePrice / 1e18;
|
|
526
|
+
|
|
527
|
+
// Calculate minimum acceptable output with slippage tolerance.
|
|
528
|
+
uint256 minEthOut = ud(fairEthOut).mul(UNIT.sub(slippageTolerance)).unwrap();
|
|
529
|
+
|
|
530
|
+
// Interaction: Swap stETH for ETH via Curve.
|
|
531
|
+
uint256 ethReceived = ICurveStETHPool(CURVE_POOL).exchange(1, 0, stETHAmount, minEthOut);
|
|
532
|
+
|
|
533
|
+
// Check: the amount of ETH received is greater than the minimum acceptable output.
|
|
534
|
+
if (ethReceived < minEthOut) {
|
|
535
|
+
revert Errors.SablierLidoAdapter_SlippageExceeded(minEthOut, ethReceived);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
wethReceived = ethReceived.toUint128();
|
|
539
|
+
|
|
540
|
+
// Interaction: Wrap ETH to get WETH.
|
|
541
|
+
IWETH9(WETH).deposit{ value: ethReceived }();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/*//////////////////////////////////////////////////////////////////////////
|
|
545
|
+
RECEIVE
|
|
546
|
+
//////////////////////////////////////////////////////////////////////////*/
|
|
547
|
+
|
|
548
|
+
receive() external payable { }
|
|
549
|
+
}
|