Skip to content

Commit fedcbc9

Browse files
committed
feat: lido withdrawal queue integration
1 parent a414a52 commit fedcbc9

File tree

6 files changed

+488
-0
lines changed

6 files changed

+488
-0
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2024.
4+
pragma solidity ^0.8.23;
5+
6+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7+
8+
import {ICreditManagerV3} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditManagerV3.sol";
9+
import {IPoolV3} from "@gearbox-protocol/core-v3/contracts/interfaces/IPoolV3.sol";
10+
11+
import {AbstractAdapter} from "../AbstractAdapter.sol";
12+
13+
import {ILidoWithdrawalQueueGateway} from "../../interfaces/lido/ILidoWithdrawalQueueGateway.sol";
14+
import {ILidoWithdrawalQueueAdapter} from "../../interfaces/lido/ILidoWithdrawalQueueAdapter.sol";
15+
16+
import {NotImplementedException} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol";
17+
18+
/// @title Lido withdrawal queue adapter
19+
/// @notice Implements logic for interacting with the Lido withdrawal queue through the gateway
20+
contract LidoWithdrawalQueueAdapter is AbstractAdapter, ILidoWithdrawalQueueAdapter {
21+
bytes32 public constant override contractType = "ADAPTER::LIDO_WITHDRAWAL_QUEUE";
22+
uint256 public constant override version = 3_10;
23+
24+
/// @notice stETH token
25+
address public immutable override stETH;
26+
27+
/// @notice wstETH token
28+
address public immutable override wstETH;
29+
30+
/// @notice WETH token
31+
address public immutable override weth;
32+
33+
/// @notice Lido withdrawal phantom token
34+
address public immutable override lidoWithdrawalPhantomToken;
35+
36+
/// @notice Constructor
37+
/// @param _creditManager Credit manager address
38+
/// @param _withdrawalQueueGateway Lido withdrawal queue gateway address
39+
/// @param _lidoWithdrawalPhantomToken Lido withdrawal phantom token address
40+
constructor(address _creditManager, address _withdrawalQueueGateway, address _lidoWithdrawalPhantomToken)
41+
AbstractAdapter(_creditManager, _withdrawalQueueGateway)
42+
{
43+
stETH = ILidoWithdrawalQueueGateway(_withdrawalQueueGateway).steth();
44+
weth = ILidoWithdrawalQueueGateway(_withdrawalQueueGateway).weth();
45+
wstETH = ILidoWithdrawalQueueGateway(_withdrawalQueueGateway).wsteth();
46+
47+
lidoWithdrawalPhantomToken = _lidoWithdrawalPhantomToken;
48+
49+
_getMaskOrRevert(stETH);
50+
_getMaskOrRevert(weth);
51+
_getMaskOrRevert(wstETH);
52+
_getMaskOrRevert(lidoWithdrawalPhantomToken);
53+
}
54+
55+
/// @notice Requests withdrawals from the Lido withdrawal queue
56+
/// @param amounts Amounts of stETH to withdraw
57+
function requestWithdrawals(uint256[] calldata amounts) external override creditFacadeOnly returns (bool) {
58+
_executeSwapSafeApprove(stETH, abi.encodeCall(ILidoWithdrawalQueueGateway.requestWithdrawals, (amounts)));
59+
return true;
60+
}
61+
62+
/// @notice Requests wstETH withdrawals from the Lido withdrawal queue
63+
function requestWithdrawalsWstETH(uint256[] calldata amounts) external override creditFacadeOnly returns (bool) {
64+
_executeSwapSafeApprove(wstETH, abi.encodeCall(ILidoWithdrawalQueueGateway.requestWithdrawalsWstETH, (amounts)));
65+
return true;
66+
}
67+
68+
/// @notice Claims the request WETH amount from the withdrawal queue gateway
69+
function claimWithdrawals(uint256 amount) external override creditFacadeOnly returns (bool) {
70+
_claimWithdrawals(amount);
71+
return false;
72+
}
73+
74+
/// @notice Withdraws phantom token for its underlying
75+
function withdrawPhantomToken(address token, uint256 amount) external override creditFacadeOnly returns (bool) {
76+
if (token != lidoWithdrawalPhantomToken) revert IncorrectStakedPhantomTokenException();
77+
_claimWithdrawals(amount);
78+
return false;
79+
}
80+
81+
/// @dev Internal implementation of `claimWithdrawals`
82+
function _claimWithdrawals(uint256 amount) internal {
83+
_execute(abi.encodeCall(ILidoWithdrawalQueueGateway.claimWithdrawals, (amount)));
84+
}
85+
86+
/// @dev It's not possible to deposit from WETH into the withdrawal phantom token,
87+
/// hence the function is not implemented.
88+
function depositPhantomToken(address, uint256) external view override creditFacadeOnly returns (bool) {
89+
revert NotImplementedException();
90+
}
91+
92+
/// @notice Serialized adapter parameters
93+
function serialize() external view returns (bytes memory serializedData) {
94+
serializedData = abi.encode(creditManager, targetContract, stETH, wstETH, weth, lidoWithdrawalPhantomToken);
95+
}
96+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2024.
4+
pragma solidity ^0.8.23;
5+
6+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
7+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8+
import {PhantomERC20} from "../PhantomERC20.sol";
9+
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
10+
import {MultiCall} from "@gearbox-protocol/core-v3/contracts/interfaces/ICreditFacadeV3.sol";
11+
import {IPhantomToken} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPhantomToken.sol";
12+
import {ILidoWithdrawalQueueGateway} from "../../interfaces/lido/ILidoWithdrawalQueueGateway.sol";
13+
14+
/// @title Lido withdrawal phantom token
15+
/// @notice Phantom ERC-20 token that represents the balance of the pending and claimable withdrawals in Lido withdrawal queue
16+
contract LidoWithdrawalPhantomToken is PhantomERC20, Ownable, IPhantomToken {
17+
bytes32 public constant override contractType = "PHANTOM_TOKEN::LIDO_WITHDRAWAL";
18+
19+
uint256 public constant override version = 3_10;
20+
21+
address public immutable withdrawalQueueGateway;
22+
23+
/// @notice Constructor
24+
/// @param _withdrawalQueueGateway The address of the Lido withdrawal queue gateway
25+
constructor(address _withdrawalQueueGateway)
26+
PhantomERC20(ILidoWithdrawalQueueGateway(_withdrawalQueueGateway).weth(), "Lido withdrawl ETH", "unstETH", 18)
27+
{
28+
withdrawalQueueGateway = _withdrawalQueueGateway;
29+
}
30+
31+
/// @notice Returns the amount of assets pending/claimable for withdrawal
32+
/// @param account The account for which the calculation is performed
33+
function balanceOf(address account) public view returns (uint256 balance) {
34+
balance = ILidoWithdrawalQueueGateway(withdrawalQueueGateway).getPendingWETH(account);
35+
}
36+
37+
/// @notice Returns phantom token's target contract and underlying
38+
function getPhantomTokenInfo() external view override returns (address, address) {
39+
return (withdrawalQueueGateway, underlying);
40+
}
41+
42+
function serialize() external view override returns (bytes memory) {
43+
return abi.encode(withdrawalQueueGateway, underlying);
44+
}
45+
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2025.
4+
pragma solidity ^0.8.23;
5+
6+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
8+
import {DoubleEndedQueue} from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol";
9+
import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol";
10+
import {IWETH} from "@gearbox-protocol/core-v3/contracts/interfaces/external/IWETH.sol";
11+
import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol";
12+
import {ReceiveIsNotAllowedException} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol";
13+
14+
import {ILidoWithdrawalQueue, WithdrawalRequestStatus} from "../../integrations/lido/ILidoWithdrawalQueue.sol";
15+
import {IstETHGetters} from "../../integrations/lido/IstETH.sol";
16+
17+
import {ILidoWithdrawalQueueGateway} from "../../interfaces/lido/ILidoWithdrawalQueueGateway.sol";
18+
19+
struct PendingWithdrawal {
20+
uint256 untransferredWETH;
21+
DoubleEndedQueue.Bytes32Deque requestIds;
22+
}
23+
24+
/// @title Lido Withdrawal Queue Gateway
25+
/// @notice Allows to redeem wstETH / stETH into WETH directly through Lido
26+
contract LidoWithdrawalQueueGateway is ILidoWithdrawalQueueGateway {
27+
using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque;
28+
using SafeERC20 for IERC20;
29+
30+
bytes32 public constant override contractType = "GATEWAY::LIDO_WITHDRAWAL_QUEUE";
31+
uint256 public constant override version = 3_10;
32+
33+
address public immutable withdrawalQueue;
34+
35+
address public immutable steth;
36+
37+
address public immutable wsteth;
38+
39+
address public immutable weth;
40+
41+
mapping(address => PendingWithdrawal) internal pendingWithdrawals;
42+
43+
constructor(address _withdrawalQueue, address _weth) {
44+
withdrawalQueue = _withdrawalQueue;
45+
weth = _weth;
46+
steth = ILidoWithdrawalQueue(withdrawalQueue).STETH();
47+
wsteth = ILidoWithdrawalQueue(withdrawalQueue).WSTETH();
48+
}
49+
50+
/// @notice Requests withdrawals from stETH in Lido queue
51+
function requestWithdrawals(uint256[] calldata amounts) external returns (uint256[] memory requestIds) {
52+
return _requestWithdrawals(amounts, false);
53+
}
54+
55+
/// @notice Requests withdrawals from wstETH in Lido queue
56+
function requestWithdrawalsWstETH(uint256[] calldata amounts) external returns (uint256[] memory requestIds) {
57+
return _requestWithdrawals(amounts, true);
58+
}
59+
60+
/// @dev Internal implementation of `requestWithdrawals` and `requestWithdrawalsWstETH`
61+
/// @dev Only 10 withdrawal requests can be active at a time to prevent too much gas
62+
/// being spent on pending WETH calculation. This is equivalent to 10000 stETH max.
63+
function _requestWithdrawals(uint256[] calldata amounts, bool isWstETH)
64+
internal
65+
returns (uint256[] memory requestIds)
66+
{
67+
if (_getRequestIds(msg.sender).length + amounts.length > 10) {
68+
revert("WithdrawalQueueGateway: Too many active withdrawals");
69+
}
70+
71+
uint256 totalAmount = 0;
72+
73+
for (uint256 i = 0; i < amounts.length; i++) {
74+
totalAmount += amounts[i];
75+
}
76+
77+
IERC20(isWstETH ? wsteth : steth).transferFrom(msg.sender, address(this), totalAmount);
78+
IERC20(isWstETH ? wsteth : steth).forceApprove(withdrawalQueue, totalAmount);
79+
80+
requestIds = isWstETH
81+
? ILidoWithdrawalQueue(withdrawalQueue).requestWithdrawalsWstETH(amounts, address(this))
82+
: ILidoWithdrawalQueue(withdrawalQueue).requestWithdrawals(amounts, address(this));
83+
84+
for (uint256 i = 0; i < requestIds.length; i++) {
85+
pendingWithdrawals[msg.sender].requestIds.pushBack(bytes32(requestIds[i]));
86+
}
87+
88+
return requestIds;
89+
}
90+
91+
/// @notice Claims finalized withdrawals from Lido and transfers the requested WETH amount
92+
/// @param amount Amount of WETH to claim
93+
/// @dev All finalized requests are removed from the list, since they are converted to untransferred WETH
94+
function claimWithdrawals(uint256 amount) external {
95+
PendingWithdrawal storage pendingWithdrawal = pendingWithdrawals[msg.sender];
96+
97+
uint256[] memory requestIds = _getRequestIds(msg.sender);
98+
99+
(uint256[] memory finalizedRequestIds,,) = _getRequestInfo(requestIds);
100+
101+
uint256[] memory hints = _getRequestHints(finalizedRequestIds);
102+
103+
uint256 balanceBefore = address(this).balance;
104+
105+
ILidoWithdrawalQueue(withdrawalQueue).claimWithdrawals(finalizedRequestIds, hints);
106+
107+
pendingWithdrawal.untransferredWETH += address(this).balance - balanceBefore;
108+
109+
if (pendingWithdrawal.untransferredWETH < amount) {
110+
revert("WithdrawalQueueGateway: Not enough WETH to claim");
111+
}
112+
113+
pendingWithdrawal.untransferredWETH -= amount;
114+
115+
IERC20(weth).transfer(msg.sender, amount);
116+
117+
for (uint256 i = 0; i < finalizedRequestIds.length; i++) {
118+
pendingWithdrawal.requestIds.popFront();
119+
}
120+
}
121+
122+
/// @notice Returns the amount of WETH that is pending withdrawal, including claimable WETH
123+
function getPendingWETH(address account) external view returns (uint256) {
124+
PendingWithdrawal storage pendingWithdrawal = pendingWithdrawals[account];
125+
126+
uint256[] memory requestIds = _getRequestIds(account);
127+
128+
(
129+
uint256[] memory finalizedRequestIds,
130+
uint256[] memory unfinalizedStETHAmounts,
131+
uint256[] memory unfinalizedShareAmounts
132+
) = _getRequestInfo(requestIds);
133+
134+
uint256[] memory hints = _getRequestHints(finalizedRequestIds);
135+
136+
uint256[] memory claimableAmounts =
137+
ILidoWithdrawalQueue(withdrawalQueue).getClaimableEther(finalizedRequestIds, hints);
138+
139+
uint256 totalAmount = 0;
140+
141+
for (uint256 i = 0; i < claimableAmounts.length; i++) {
142+
totalAmount += claimableAmounts[i];
143+
}
144+
145+
uint256 totalShares = IstETHGetters(steth).getTotalShares();
146+
uint256 totalPooledEther = IstETHGetters(steth).getTotalPooledEther();
147+
148+
for (uint256 i = 0; i < unfinalizedStETHAmounts.length; i++) {
149+
uint256 amountByShares = unfinalizedShareAmounts[i] * totalPooledEther / totalShares;
150+
totalAmount += amountByShares < unfinalizedStETHAmounts[i] ? amountByShares : unfinalizedStETHAmounts[i];
151+
}
152+
153+
totalAmount += pendingWithdrawal.untransferredWETH;
154+
155+
return totalAmount;
156+
}
157+
158+
/// @notice Returns the amount of claimable WETH
159+
function getClaimableWETH(address account) external view returns (uint256) {
160+
PendingWithdrawal storage pendingWithdrawal = pendingWithdrawals[account];
161+
162+
uint256[] memory requestIds = _getRequestIds(account);
163+
164+
(uint256[] memory finalizedRequestIds,,) = _getRequestInfo(requestIds);
165+
166+
uint256[] memory hints = _getRequestHints(finalizedRequestIds);
167+
168+
uint256[] memory claimableAmounts =
169+
ILidoWithdrawalQueue(withdrawalQueue).getClaimableEther(finalizedRequestIds, hints);
170+
171+
uint256 totalAmount = 0;
172+
173+
for (uint256 i = 0; i < claimableAmounts.length; i++) {
174+
totalAmount += claimableAmounts[i];
175+
}
176+
177+
totalAmount += pendingWithdrawal.untransferredWETH;
178+
179+
return totalAmount;
180+
}
181+
182+
/// @dev Returns the IDs of finalized requests, as well as stETH and share amounts of unfinalized requests
183+
function _getRequestInfo(uint256[] memory requestIds)
184+
internal
185+
view
186+
returns (
187+
uint256[] memory finalizedRequestIds,
188+
uint256[] memory unfinalizedStETHAmounts,
189+
uint256[] memory unfinalizedShareAmounts
190+
)
191+
{
192+
WithdrawalRequestStatus[] memory statuses =
193+
ILidoWithdrawalQueue(withdrawalQueue).getWithdrawalStatus(requestIds);
194+
195+
uint256 finalizedCount = 0;
196+
197+
for (uint256 i = 0; i < requestIds.length; i++) {
198+
if (statuses[i].isFinalized) {
199+
finalizedCount++;
200+
}
201+
}
202+
203+
finalizedRequestIds = new uint256[](finalizedCount);
204+
205+
uint256 index = 0;
206+
207+
for (uint256 i = 0; i < requestIds.length; i++) {
208+
if (statuses[i].isFinalized) {
209+
finalizedRequestIds[index] = requestIds[i];
210+
index++;
211+
}
212+
}
213+
214+
unfinalizedStETHAmounts = new uint256[](requestIds.length - finalizedCount);
215+
unfinalizedShareAmounts = new uint256[](requestIds.length - finalizedCount);
216+
217+
index = 0;
218+
219+
for (uint256 i = finalizedCount; i < requestIds.length; i++) {
220+
unfinalizedStETHAmounts[index] = statuses[i].amountOfStETH;
221+
unfinalizedShareAmounts[index] = statuses[i].amountOfShares;
222+
index++;
223+
}
224+
225+
return (finalizedRequestIds, unfinalizedStETHAmounts, unfinalizedShareAmounts);
226+
}
227+
228+
/// @dev Returns the IDs of the active withdrawal requests for an account
229+
function _getRequestIds(address account) internal view returns (uint256[] memory requestIds) {
230+
PendingWithdrawal storage pendingWithdrawal = pendingWithdrawals[account];
231+
uint256 numRequests = pendingWithdrawal.requestIds.length();
232+
requestIds = new uint256[](numRequests);
233+
for (uint256 i = 0; i < numRequests; i++) {
234+
requestIds[i] = uint256(pendingWithdrawal.requestIds.at(i));
235+
}
236+
return requestIds;
237+
}
238+
239+
/// @dev Returns the checkpoint hints for the active withdrawal requests
240+
function _getRequestHints(uint256[] memory requestIds) internal view returns (uint256[] memory hints) {
241+
uint256 lastCheckpointIndex = ILidoWithdrawalQueue(withdrawalQueue).getLastCheckpointIndex();
242+
return ILidoWithdrawalQueue(withdrawalQueue).findCheckpointHints(requestIds, 0, lastCheckpointIndex);
243+
}
244+
245+
receive() external payable {
246+
if (msg.sender != withdrawalQueue) {
247+
revert("WithdrawalQueueGateway: Only withdrawal queue can send ETH");
248+
}
249+
250+
IWETH(weth).deposit{value: msg.value}();
251+
}
252+
}

0 commit comments

Comments
 (0)