-
Notifications
You must be signed in to change notification settings - Fork 80
fix: add disconnect guide #847
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
loga4
wants to merge
6
commits into
main
Choose a base branch
from
feat/strategy-dw-closing
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+332
−0
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
ad2a924
fix: add disconnect guide
loga4 0a2c15a
fix: distribute command add space
loga4 fb3828e
fix: update docs for buiders
loga4 e5b4b6c
fix: update disconnect guide
Jeday ffc2106
fix: review comments
loga4 1705a33
fix: added stv-steth for pause-deposits and pause-minting commands
loga4 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
332 changes: 332 additions & 0 deletions
332
run-on-lido/stvaults/building-guides/pooled-staking-product/disconnect-guide.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,332 @@ | ||
| --- | ||
| sidebar_position: 5 | ||
| --- | ||
|
|
||
| # 🔌 DeFi Wrapper Disconnect Guide | ||
|
|
||
| This guide walks through the full process of disconnecting a DeFi Wrapper (pooled staking product) from the Lido protocol and distributing remaining assets to users. | ||
|
|
||
| :::warning | ||
| Disconnecting a DeFi Wrapper is an irreversible operation that affects all pool users. Ensure you communicate the timeline and plan to your users well in advance. | ||
| ::: | ||
|
|
||
| ## Overview | ||
|
|
||
| 1. **Assign required roles** to a trusted actor via the Timelock Controller. | ||
| 2. **Exit all validators** (voluntarily or forcibly). | ||
| 3. **Pause withdrawals** on the Withdrawal Queue and **finalize all pending withdrawal requests**. | ||
| 4. **Pause deposits and minting** on the Pool contract. | ||
| 5. **Rebalance the Staking Vault** to zero liability. | ||
| 6. **Disconnect the stVault** — follow the [stVault Disconnect Guide](../../operational-and-management-guides/stvault-disconnect-guide.md) (initiate voluntary disconnect, apply oracle report, abandon Dashboard, accept ownership). | ||
| 7. **Withdraw assets** from the Staking Vault and **distribute them to users** via the Distributor. | ||
|
|
||
| Steps 1–5 are DeFi Wrapper-specific and covered below. Step 6 follows the standard stVault disconnect flow. Step 7 covers asset distribution and user claiming. | ||
|
|
||
| --- | ||
|
|
||
| ## Step 1. Assign required roles | ||
|
|
||
| The disconnect process requires multiple roles across the Pool, Withdrawal Queue, and Dashboard contracts. Grant these roles to a trusted actor via the Timelock Controller. | ||
|
|
||
| | Role | Contract | Purpose | | ||
| | ----------------------------------- | ---------------- | ----------------------------------------- | | ||
| | `LOSS_SOCIALIZER_ROLE` | Pool | Force rebalance undercollateralized users | | ||
| | `DEPOSITS_PAUSE_ROLE` | Pool | Pause new deposits | | ||
| | `MINTING_PAUSE_ROLE` | Pool | Pause stETH minting | | ||
| | `WITHDRAWALS_PAUSE_ROLE` | Withdrawal Queue | Pause new withdrawal requests | | ||
| | `FINALIZE_ROLE` | Withdrawal Queue | Finalize pending withdrawal requests | | ||
| | `TRIGGER_VALIDATOR_WITHDRAWAL_ROLE` | Dashboard | Force validator exits | | ||
| | `REBALANCE_ROLE` | Dashboard | Rebalance the vault | | ||
|
|
||
| Schedule and execute a batch transaction through the Timelock Controller to grant all roles at once: | ||
|
|
||
| ``` | ||
| targets: [Pool, Pool, Pool, WithdrawalQueue, WithdrawalQueue, Dashboard, Dashboard] | ||
| payloads: [ | ||
| grantRole(LOSS_SOCIALIZER_ROLE, trustedActor), | ||
| grantRole(DEPOSITS_PAUSE_ROLE, trustedActor), | ||
| grantRole(MINTING_PAUSE_ROLE, trustedActor), | ||
| grantRole(WITHDRAWALS_PAUSE_ROLE, trustedActor), | ||
| grantRole(FINALIZE_ROLE, trustedActor), | ||
| grantRole(TRIGGER_VALIDATOR_WITHDRAWAL_ROLE, trustedActor), | ||
| grantRole(REBALANCE_ROLE, trustedActor) | ||
| ] | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Step 2. Exit all validators | ||
|
|
||
| Exit all validators associated with the Staking Vault. This moves ETH from the Beacon Chain back to the vault balance. | ||
|
|
||
| - **Voluntary exit:** Request exits through your standard validator management tooling. | ||
| - **Forced exit:** If voluntary exits are not possible, call `Dashboard.triggerValidatorWithdrawals()` from an account with `TRIGGER_VALIDATOR_WITHDRAWAL_ROLE`: | ||
|
|
||
| ```bash | ||
| yarn start contracts dashboard w trigger-v-w <dashboardAddress> <pubkeys> <amounts> <refundAddress> | ||
| ``` | ||
|
|
||
| :::info | ||
| The `triggerValidatorWithdrawals` call requires sending a small withdrawal fee (currently ~10 gwei per validator) to cover the EIP-7002 triggerable withdrawal fee. | ||
| ::: | ||
|
|
||
| Wait for all validator exits to complete and ETH to be swept back to the Staking Vault balance before proceeding. | ||
|
|
||
| --- | ||
|
|
||
| ## Step 3. Pause withdrawals and finalize pending requests | ||
|
|
||
| ### 3.1. Pause new withdrawal requests | ||
|
|
||
| Prevent users from creating new withdrawal requests by calling `WithdrawalQueue.pauseWithdrawals()` from an account with `WITHDRAWALS_PAUSE_ROLE`: | ||
|
|
||
| ```bash | ||
| yarn start dw c wq w pause <withdrawalQueueAddress> | ||
| ``` | ||
|
|
||
| ### 3.2. Finalize all pending withdrawal requests | ||
|
|
||
| Before disconnecting, all pending withdrawal requests must be finalized. | ||
|
|
||
| An oracle report may be required before finalization to update the vault state. Apply a fresh report if needed. | ||
|
|
||
| Call `WithdrawalQueue.finalize(maxRequests, gasCostCoverageRecipient)` from an account with `FINALIZE_ROLE`: | ||
|
|
||
| ```bash | ||
| yarn start dw c wq w finalize <withdrawalQueueAddress> <maxRequests> <gasCostCoverageRecipient> | ||
| ``` | ||
|
|
||
| After finalization, verify all requests are processed — `unfinalizedRequestsNumber()`, `unfinalizedStv()`, `unfinalizedAssets()`, and `unfinalizedStethShares()` should all return `0`. | ||
|
|
||
| --- | ||
|
|
||
| ## Step 4. Pause deposits and minting | ||
|
|
||
| ### 4.1. Pause deposits | ||
|
|
||
| Call `Pool.pauseDeposits()` from an account with `DEPOSITS_PAUSE_ROLE`: | ||
|
|
||
| ```bash | ||
| yarn start dw c stv-steth w pause-deposits <poolAddress> | ||
| ``` | ||
|
|
||
| ### 4.2. Pause minting | ||
|
|
||
| Call `Pool.pauseMinting()` from an account with `MINTING_PAUSE_ROLE`: | ||
|
|
||
| ```bash | ||
| yarn start dw c stv-steth w pause-minting <poolAddress> | ||
| ``` | ||
|
|
||
| After pausing, any attempts to deposit ETH, mint stETH shares, or mint wstETH will revert. | ||
|
|
||
| --- | ||
|
|
||
| ## Step 5. Rebalance the Staking Vault to zero liability | ||
|
|
||
| ### 5.1. Force rebalance undercollateralized users (if any) | ||
|
|
||
| If any pool users are undercollateralized, force rebalance them by calling `Pool.forceRebalanceAndSocializeLoss()` from an account with `LOSS_SOCIALIZER_ROLE`. | ||
|
|
||
| ### 5.2. Rebalance the vault | ||
|
|
||
| Check the current liability: | ||
|
|
||
| ```bash | ||
| yarn start contracts dashboard r liability-shares <dashboardAddress> | ||
| ``` | ||
|
|
||
| Call `Dashboard.rebalanceVaultWithShares(liabilityShares)` from an account with `REBALANCE_ROLE`, passing the full `liabilityShares` amount to bring the liability to zero: | ||
|
|
||
| ```bash | ||
| yarn start contracts dashboard w rebalance-shares <dashboardAddress> <liabilityShares> | ||
| ``` | ||
|
|
||
| :::warning | ||
| The disconnect will revert with `NoLiabilitySharesShouldBeLeft` if any liability shares remain. Ensure `Dashboard.liabilityShares()` returns `0` before proceeding. | ||
| ::: | ||
|
|
||
| --- | ||
|
|
||
| ## Step 6. Disconnect the stVault | ||
|
|
||
| Follow the [stVault Disconnect Guide](../../operational-and-management-guides/stvault-disconnect-guide.md) to complete the disconnection: | ||
|
|
||
| 1. **Initiate voluntary disconnect** — schedule and execute `Dashboard.voluntaryDisconnect()` through the Timelock Controller. Requires a fresh oracle report. | ||
| 2. **Apply the next oracle report** — finalizes the disconnection. | ||
| 3. **Abandon Dashboard** — call `Dashboard.abandonDashboard(newOwner)` from the Timelock Controller. | ||
| 4. **Accept ownership** — call `StakingVault.acceptOwnership()` from the `newOwner` address. | ||
|
|
||
| --- | ||
|
|
||
| ## Step 7. Withdraw assets and distribute to users | ||
|
|
||
| After disconnection, remaining ETH in the vault must be distributed to pool users through the Distributor contract. | ||
|
|
||
| ### 7.1. Get the Distributor address | ||
|
|
||
| ```bash | ||
| yarn start dw c stv r DISTRIBUTOR <poolAddress> | ||
| ``` | ||
|
|
||
| ### 7.2. Convert vault ETH to wstETH | ||
|
|
||
| The Distributor contract does not accept raw ETH, so the vault balance must be converted to wstETH first. | ||
|
|
||
| First, retrieve the available balance of the vault: | ||
|
|
||
| ```bash | ||
| yarn start contracts vault r available-balance <vaultAddress> | ||
| ``` | ||
|
|
||
| Call `StakingVault.withdraw(recipient, amount)` with the **wstETH contract address** as the recipient. The wstETH contract has a `receive()` function that automatically stakes incoming ETH into stETH and mints wstETH back to the sender (the vault): | ||
|
|
||
| ```bash | ||
| yarn start contracts vault w withdraw <vaultAddress> <wstethAddress> <amountInETH> | ||
| ``` | ||
|
|
||
| After this call, the vault holds wstETH tokens (not ETH). | ||
|
|
||
| :::info | ||
| Make sure you account for the Initial Connect Deposit (1 ETH) that was unlocked after disconnect — it is now part of the available balance. | ||
loga4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ::: | ||
|
|
||
| ### 7.3. Transfer wstETH to the Distributor | ||
|
|
||
| First, retrieve the wstETH balance of the vault: | ||
|
|
||
| ```bash | ||
| yarn start account r info <vaultAddress> | ||
| ``` | ||
|
|
||
| Then send the wstETH from the vault to the Distributor contract using `collectERC20`, passing the retrieved `<wstethAmount>`: | ||
|
|
||
| ```bash | ||
| yarn start contracts vault w collect-erc20 <vaultAddress> <wstethAddress> <wstethAmount> <distributorAddress> | ||
| ``` | ||
|
|
||
| ### 7.4. Add wstETH as a supported distribution token | ||
|
|
||
| If wstETH is not yet registered in the Distributor, add it: | ||
|
|
||
| ```bash | ||
| yarn start dw uc distributor w add-token <poolAddress> <wstethAddress> | ||
| ``` | ||
|
|
||
| ### 7.5. Generate the Merkle tree, upload to IPFS, and set the root | ||
|
|
||
| The CLI provides a single command that handles the entire distribution flow: | ||
|
|
||
| 1. Calculates each user's share based on their balance at the time of distribution. | ||
| 2. Builds a Merkle tree mapping each user to their cumulative claimable amount. | ||
| 3. Transfers tokens to the Distributor contract (if not already transferred). | ||
| 4. Sets the Merkle root and CID on-chain. | ||
| 5. Saves file locally so you can upload and pin to IPFS provider of choice | ||
|
|
||
| ```bash | ||
| yarn start dw uc distributor w distribute <poolAddress> <wstethAddress> <amount> \ | ||
| --mode=snapshot \ | ||
| --output-path ./distribution.json | ||
| ``` | ||
|
|
||
| **Options:** | ||
|
|
||
| | Option | Description | | ||
| | --------------------------------------------- | ------------------------------------------------------------------------- | | ||
| | `--blacklist <addresses>` | Addresses to exclude from distribution | | ||
| | `--from-block <block>` / `--to-block <block>` | Block range for processing transfer events | | ||
| | `--output-path <path>` | Path to save the distribution JSON | | ||
| | `--upload [pinningUrl]` | Upload the Merkle tree to an IPFS pinning service | | ||
| | `--skip-transfer` | Skip transferring tokens to the Distributor (if already done in step 7.3) | | ||
| | `--skip-set-root` | Generate the tree without setting the root on-chain | | ||
| | `--skip-write` | Skip writing the distribution JSON to file | | ||
|
|
||
| :::info | ||
| Since tokens were already transferred in step 7.3, use `--skip-transfer` to avoid a duplicate transfer: | ||
|
|
||
| ```bash | ||
| yarn start dw uc distributor w distribute <poolAddress> <wstethAddress> <amount> \ | ||
loga4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| --skip-transfer \ | ||
| --mode=snapshot \ | ||
| --output-path=<path> | ||
| ``` | ||
|
|
||
| ::: | ||
|
|
||
| The caller must have `MANAGER_ROLE` on the Distributor contract. | ||
|
|
||
| ### 7.6. Verify the distribution | ||
|
|
||
| Check the Distributor state to confirm the distribution was successful: | ||
|
|
||
| ```bash | ||
| yarn start dw uc distributor r state <poolAddress> | ||
| ``` | ||
|
|
||
| Verify the following fields in the output: | ||
|
|
||
| - **Merkle Root** — must be a non-zero value, indicating the Merkle tree has been set | ||
| - **CID** — must contain a valid IPFS CID, confirming the distribution data was uploaded to IPFS. You can open the CID via an IPFS gateway to inspect which tokens and amounts were distributed | ||
| - **Last Processed Block** — shows the block number at which the distribution was made | ||
|
|
||
| ### 7.7 Upload distribution to IPFS and pin the file | ||
|
|
||
| When uploading the distribution to IPFS it's important to set CID to v0 format. | ||
|
|
||
| --- | ||
|
|
||
| ## User: claiming funds | ||
|
|
||
| After the operator has distributed assets and published the Merkle tree, users can claim their share on the UI. | ||
|
|
||
| :::info | ||
| For `stvStrategyPool` users must perform first step of withdrawal via UI to request funds back from underlying DeFi-strategy to proxy balance. | ||
| ::: | ||
|
|
||
| ### Claiming with UI | ||
|
|
||
| Even when vault is disconnected users will be able to use UI: | ||
|
|
||
| - request and claim withdrawals from underlying strategy vaults | ||
| - claim any previous claimable withdrawals from pool's `WithdrawalQueue` | ||
| - claim any distributed funds. In case of `stvStrategyPool` token are distributed to proxies but funds can be claimed via UI | ||
|
|
||
| ### Claiming with CLI | ||
|
|
||
| If you want you can claim funds on behalf of the users via CLI, but this will produce 1 transaction per user per token(batch transactions are supported via CLI and WalletConnect) | ||
|
|
||
| Claim: | ||
|
|
||
| ```bash | ||
| yarn start dw uc distributor w claim <poolAddress> | ||
| ``` | ||
|
|
||
| You can adjust command with options: | ||
|
|
||
| - `--recipients [addresses...]` - listing only specific address to claim for | ||
| - `--tokens [addresses...]` - listing only specific tokens to claim | ||
| - `--print-only` - only print planned claim | ||
|
|
||
| ### Claiming ETH from previously requested withdrawals with CLI | ||
|
|
||
| If the user had requested withdrawals before the disconnect, those requests were finalized by the operator during [Step 3](#step-3-pause-withdrawals-and-finalize-pending-requests). The ETH is ready but still held by the Withdrawal Queue — the user must explicitly claim it to receive it in their wallet. | ||
|
|
||
| First, retrieve the user's withdrawal request IDs: | ||
|
|
||
| ```bash | ||
| yarn start dw c wq r withdrawalRequestsOf <withdrawalQueueAddress> <ownerAddress> | ||
| ``` | ||
|
|
||
loga4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Then claim the withdrawal(s): | ||
|
|
||
| ```bash | ||
| # Claim a single request | ||
| yarn start dw c wq w claim-withdrawal <withdrawalQueueAddress> <requestId> <recipientAddress> | ||
loga4 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # Claim multiple requests | ||
| yarn start dw c wq w claim-withdrawals <withdrawalQueueAddress> <requestIds> <hints> <recipientAddress> | ||
| ``` | ||
|
|
||
| :::info | ||
| The Withdrawal Queue remains functional for claims even after the pool is disconnected. Users can claim at any time. | ||
| ::: | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.