Skip to content

Commit aeeb5b0

Browse files
authored
Lido withdraw strategy (#2080)
* Added LidoWithdrawalStrategy * Added deployment of Lido withdrawal strategy * Completed Lido Withdrawal Strategy fork tests * Updated Natspec * Renamed fraxEth to stEth in LidoWithdrawalStrategy * Moved _abstractSetPToken to bottom of the LidoWithdrawalStrategy made _abstractSetPToken pure * renamed numWithdrawals to withdrawalLength * outstandingWithdrawals now accounts for stETH dust left behind * don't format Defender Action code in dist folder
1 parent c96032b commit aeeb5b0

File tree

6 files changed

+698
-43
lines changed

6 files changed

+698
-43
lines changed

contracts/contracts/proxies/Proxies.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,10 @@ contract NativeStakingFeeAccumulatorProxy is
227227
{
228228

229229
}
230+
231+
/**
232+
* @notice LidoWithdrawalStrategyProxy delegates calls to a LidoWithdrawalStrategy implementation
233+
*/
234+
contract LidoWithdrawalStrategyProxy is InitializeGovernedUpgradeabilityProxy {
235+
236+
}
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import { IERC20, InitializableAbstractStrategy } from "../utils/InitializableAbstractStrategy.sol";
5+
import { IWETH9 } from "../interfaces/IWETH9.sol";
6+
import { IVault } from "../interfaces/IVault.sol";
7+
8+
interface IStETHWithdrawal {
9+
event WithdrawalRequested(
10+
uint256 indexed requestId,
11+
address indexed requestor,
12+
address indexed owner,
13+
uint256 amountOfStETH,
14+
uint256 amountOfShares
15+
);
16+
event WithdrawalsFinalized(
17+
uint256 indexed from,
18+
uint256 indexed to,
19+
uint256 amountOfETHLocked,
20+
uint256 sharesToBurn,
21+
uint256 timestamp
22+
);
23+
event WithdrawalClaimed(
24+
uint256 indexed requestId,
25+
address indexed owner,
26+
address indexed receiver,
27+
uint256 amountOfETH
28+
);
29+
30+
struct WithdrawalRequestStatus {
31+
/// @notice stETH token amount that was locked on withdrawal queue for this request
32+
uint256 amountOfStETH;
33+
/// @notice amount of stETH shares locked on withdrawal queue for this request
34+
uint256 amountOfShares;
35+
/// @notice address that can claim or transfer this request
36+
address owner;
37+
/// @notice timestamp of when the request was created, in seconds
38+
uint256 timestamp;
39+
/// @notice true, if request is finalized
40+
bool isFinalized;
41+
/// @notice true, if request is claimed. Request is claimable if (isFinalized && !isClaimed)
42+
bool isClaimed;
43+
}
44+
45+
function requestWithdrawals(uint256[] calldata _amounts, address _owner)
46+
external
47+
returns (uint256[] memory requestIds);
48+
49+
function getLastCheckpointIndex() external view returns (uint256);
50+
51+
function findCheckpointHints(
52+
uint256[] calldata _requestIds,
53+
uint256 _firstIndex,
54+
uint256 _lastIndex
55+
) external view returns (uint256[] memory hintIds);
56+
57+
function claimWithdrawals(
58+
uint256[] calldata _requestIds,
59+
uint256[] calldata _hints
60+
) external;
61+
62+
function getWithdrawalStatus(uint256[] calldata _requestIds)
63+
external
64+
view
65+
returns (WithdrawalRequestStatus[] memory statuses);
66+
67+
function getWithdrawalRequests(address _owner)
68+
external
69+
view
70+
returns (uint256[] memory requestsIds);
71+
72+
function finalize(
73+
uint256 _lastRequestIdToBeFinalized,
74+
uint256 _maxShareRate
75+
) external payable;
76+
}
77+
78+
/**
79+
* @title Lido Withdrawal Strategy
80+
* @notice This strategy withdraws ETH from stETH via the Lido Withdrawal Queue contract
81+
* @author Origin Protocol Inc
82+
*/
83+
contract LidoWithdrawalStrategy is InitializableAbstractStrategy {
84+
/// @notice Address of the WETH token
85+
IWETH9 private constant weth =
86+
IWETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
87+
/// @notice Address of the stETH token
88+
IERC20 private constant stETH =
89+
IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);
90+
/// @notice Address of the Lido Withdrawal Queue contract
91+
IStETHWithdrawal private constant withdrawalQueue =
92+
IStETHWithdrawal(0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1);
93+
/// @notice Maximum amount of stETH that can be withdrawn in a single request
94+
uint256 public constant MaxWithdrawalAmount = 1000 ether;
95+
/// @notice Total amount of stETH that has been requested to be withdrawn for ETH
96+
uint256 public outstandingWithdrawals;
97+
98+
event WithdrawalRequests(uint256[] requestIds, uint256[] amounts);
99+
event WithdrawalClaims(uint256[] requestIds, uint256 amount);
100+
101+
constructor(BaseStrategyConfig memory _stratConfig)
102+
InitializableAbstractStrategy(_stratConfig)
103+
{
104+
require(MaxWithdrawalAmount < type(uint120).max);
105+
}
106+
107+
/**
108+
* @notice initialize function, to set up initial internal state
109+
* @param _rewardTokenAddresses Address of reward token for platform
110+
* @param _assets Addresses of initial supported assets
111+
* @param _pTokens Platform Token corresponding addresses
112+
*/
113+
function initialize(
114+
address[] memory _rewardTokenAddresses,
115+
address[] memory _assets,
116+
address[] memory _pTokens
117+
) external onlyGovernor initializer {
118+
InitializableAbstractStrategy._initialize(
119+
_rewardTokenAddresses,
120+
_assets,
121+
_pTokens
122+
);
123+
safeApproveAllTokens();
124+
}
125+
126+
/**
127+
* @notice deposit() function not used for this strategy. Use depositAll() instead.
128+
*/
129+
function deposit(address, uint256) public override onlyVault nonReentrant {
130+
// This method no longer used by the VaultAdmin, and we don't want it
131+
// to be used by VaultCore.
132+
require(false, "use depositAll() instead");
133+
}
134+
135+
/**
136+
* @notice Takes all given stETH and creates Lido withdrawal request
137+
*/
138+
function depositAll() external override onlyVault nonReentrant {
139+
uint256 stETHStart = stETH.balanceOf(address(this));
140+
require(stETHStart > 0, "No stETH to withdraw");
141+
142+
uint256 withdrawalLength = (stETHStart / MaxWithdrawalAmount) + 1;
143+
uint256[] memory amounts = new uint256[](withdrawalLength);
144+
145+
uint256 stETHRemaining = stETHStart;
146+
uint256 i = 0;
147+
while (stETHRemaining > MaxWithdrawalAmount) {
148+
amounts[i++] = MaxWithdrawalAmount;
149+
stETHRemaining -= MaxWithdrawalAmount;
150+
}
151+
amounts[i] = stETHRemaining;
152+
153+
uint256[] memory requestIds = withdrawalQueue.requestWithdrawals(
154+
amounts,
155+
address(this)
156+
);
157+
158+
emit WithdrawalRequests(requestIds, amounts);
159+
160+
// Is there any stETH left except 1 wei from each request?
161+
// This is because stETH does not transfer all the transfer amount.
162+
uint256 stEthDust = stETH.balanceOf(address(this));
163+
require(
164+
stEthDust <= withdrawalLength,
165+
"Not all stEth in withdraw queue"
166+
);
167+
outstandingWithdrawals += stETHStart - stEthDust;
168+
169+
// This strategy claims to support WETH, so it is possible for
170+
// the vault to transfer WETH to it. This returns any deposited WETH
171+
// to the vault so that it is not lost for balance tracking purposes.
172+
uint256 wethBalance = weth.balanceOf(address(this));
173+
if (wethBalance > 0) {
174+
// slither-disable-next-line unchecked-transfer
175+
weth.transfer(vaultAddress, wethBalance);
176+
}
177+
178+
emit Deposit(address(stETH), address(withdrawalQueue), stETHStart);
179+
}
180+
181+
/**
182+
* @notice Withdraw an asset from the underlying platform
183+
* @param _recipient Address to receive withdrawn assets
184+
* @param _asset Address of the asset to withdraw
185+
* @param _amount Amount of assets to withdraw
186+
*/
187+
function withdraw(
188+
// solhint-disable-next-line no-unused-vars
189+
address _recipient,
190+
// solhint-disable-next-line no-unused-vars
191+
address _asset,
192+
// solhint-disable-next-line no-unused-vars
193+
uint256 _amount
194+
) external override onlyVault nonReentrant {
195+
// Does nothing - all withdrawals need to be called manually using the
196+
// Strategist calling claimWithdrawals
197+
revert("use claimWithdrawals()");
198+
}
199+
200+
/**
201+
* @notice Claim previously requested withdrawals that have now finalized.
202+
* Called by the Strategist.
203+
* @param _requestIds Array of withdrawal request identifiers
204+
* @param expectedAmount Total amount of ETH expect to be withdrawn
205+
*/
206+
function claimWithdrawals(
207+
uint256[] memory _requestIds,
208+
uint256 expectedAmount
209+
) external nonReentrant {
210+
require(
211+
msg.sender == IVault(vaultAddress).strategistAddr(),
212+
"Caller is not the Strategist"
213+
);
214+
uint256 startingBalance = payable(address(this)).balance;
215+
uint256 lastIndex = withdrawalQueue.getLastCheckpointIndex();
216+
uint256[] memory hintIds = withdrawalQueue.findCheckpointHints(
217+
_requestIds,
218+
1,
219+
lastIndex
220+
);
221+
withdrawalQueue.claimWithdrawals(_requestIds, hintIds);
222+
223+
uint256 currentBalance = payable(address(this)).balance;
224+
uint256 withdrawalAmount = currentBalance - startingBalance;
225+
// Withdrawal amount should be within 2 wei of expected amount
226+
require(
227+
withdrawalAmount + 2 >= expectedAmount &&
228+
withdrawalAmount <= expectedAmount,
229+
"Withdrawal amount not expected"
230+
);
231+
232+
emit WithdrawalClaims(_requestIds, withdrawalAmount);
233+
234+
outstandingWithdrawals -= withdrawalAmount;
235+
weth.deposit{ value: currentBalance }();
236+
// slither-disable-next-line unchecked-transfer
237+
weth.transfer(vaultAddress, currentBalance);
238+
emit Withdrawal(
239+
address(weth),
240+
address(withdrawalQueue),
241+
currentBalance
242+
);
243+
}
244+
245+
/**
246+
* @notice Withdraw all assets from this strategy, and transfer to the Vault.
247+
* In correct operation, this strategy should never hold any assets.
248+
*/
249+
function withdrawAll() external override onlyVaultOrGovernor nonReentrant {
250+
if (payable(address(this)).balance > 0) {
251+
weth.deposit{ value: payable(address(this)).balance }();
252+
}
253+
uint256 wethBalance = weth.balanceOf(address(this));
254+
if (wethBalance > 0) {
255+
// slither-disable-next-line unchecked-transfer
256+
weth.transfer(vaultAddress, wethBalance);
257+
emit Withdrawal(address(weth), address(0), wethBalance);
258+
}
259+
uint256 stEthBalance = stETH.balanceOf(address(this));
260+
if (stEthBalance > 0) {
261+
// slither-disable-next-line unchecked-transfer
262+
stETH.transfer(vaultAddress, stEthBalance);
263+
emit Withdrawal(address(stETH), address(0), stEthBalance);
264+
}
265+
}
266+
267+
/**
268+
* @notice Returns the amount of queued stETH that will be returned as WETH.
269+
* We return this as a WETH asset, since that is what it will eventually be returned as.
270+
* We only return the outstandingWithdrawals, because the contract itself should never hold any funds.
271+
* @param _asset Address of the asset
272+
* @return balance Total value of the asset in the platform
273+
*/
274+
function checkBalance(address _asset)
275+
external
276+
view
277+
override
278+
returns (uint256 balance)
279+
{
280+
if (_asset == address(weth)) {
281+
return outstandingWithdrawals;
282+
} else if (_asset == address(stETH)) {
283+
return 0;
284+
} else {
285+
revert("Unexpected asset address");
286+
}
287+
}
288+
289+
/**
290+
* @notice Approve the spending of all assets by their corresponding cToken,
291+
* if for some reason is it necessary.
292+
*/
293+
function safeApproveAllTokens() public override {
294+
// slither-disable-next-line unused-return
295+
stETH.approve(address(withdrawalQueue), type(uint256).max);
296+
}
297+
298+
/**
299+
* @notice Check if an asset is supported.
300+
* @param _asset Address of the asset
301+
* @return bool Whether asset is supported
302+
*/
303+
function supportsAsset(address _asset) public pure override returns (bool) {
304+
// stETH can be deposited by the vault and balances are reported in WETH
305+
return _asset == address(stETH) || _asset == address(weth);
306+
}
307+
308+
/// @notice Needed to receive ETH when withdrawal requests are claimed
309+
receive() external payable {}
310+
311+
function _abstractSetPToken(address, address) internal pure override {
312+
revert("No pTokens are used");
313+
}
314+
}

0 commit comments

Comments
 (0)