Skip to content

Commit 530d524

Browse files
authored
Merge pull request #784 from lidofinance/feat/dw-operations
docs: update pooled staking product documentation and add roles, perm…
2 parents ff014e7 + 8f4fa7b commit 530d524

File tree

6 files changed

+601
-30
lines changed

6 files changed

+601
-30
lines changed

run-on-lido/stvaults/building-guides/index.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ sidebar_position: 1
66

77
The stVaults platform enables the creation of staking products tailored to different target audiences with diverse needs. These two comprehensive guides provide detailed instructions on how to create any product powered by stVaults.
88

9-
| Staking Product to build | Its Value Proposition | Its Segments |
10-
| -------- | -------- | -------- |
11-
| [Basic stVault with optional liquidity](./basic-stvault.md) | A competitive alternative to native staking: users stake with the same Node Operator while gaining optional liquidity through stETH. | Institutional stakers, large individual stakers (32+ ETH), funds, treasuries, builders, integrators, liquidity providers. |
12-
| [End-user staking product by DeFi Wrapper](./pooled-staking-product.md) | A DeFi-wrapped stVault with pooling, liquidity, automated yield-boosting strategy, and white-labeled staking UI – all in one customizable no-code/low-code solution. | Retail stakers (<32 ETH), APR-maximizers, institutional stakers seeking a simple staking UI. |
9+
| Staking Product to build | Its Value Proposition | Its Segments |
10+
|-----------------------------------------------------------------------| -------- | -------- |
11+
| [Basic stVault with optional liquidity](./basic-stvault.md) | A competitive alternative to native staking: users stake with the same Node Operator while gaining optional liquidity through stETH. | Institutional stakers, large individual stakers (32+ ETH), funds, treasuries, builders, integrators, liquidity providers. |
12+
| [End-user staking product by DeFi Wrapper](./pooled-staking-product/) | A DeFi-wrapped stVault with pooling, liquidity, automated yield-boosting strategy, and white-labeled staking UI – all in one customizable no-code/low-code solution. | Retail stakers (<32 ETH), APR-maximizers, institutional stakers seeking a simple staking UI. |
1313

1414
:::info
1515
Have your own custom product in mind? [Contact us](https://tally.so/r/mVrkZa)!
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
---
2+
sidebar_position: 4
3+
---
4+
5+
import Tabs from '@theme/Tabs';
6+
import TabItem from '@theme/TabItem';
7+
8+
# 🧩 Pool with a custom strategy
9+
10+
## Intro
11+
12+
This guide walks through how to build and deploy a **pooled staking product with a custom yield strategy** using the DeFi Wrapper toolkit.
13+
14+
The DeFi Wrapper architecture is designed to support **any custom strategy** as long as it implements the required interfaces.
15+
16+
There are **two paths** to getting a pool with a custom strategy:
17+
18+
1. [**Deploy from scratch**](#path-a-deploy-a-new-pool-with-custom-strategy) - Already have a custom strategy and ready to launch a pool
19+
20+
2. [**Upgrade existing pool**](#path-b-upgrade-an-existing-pool-to-a-strategy-pool) - Create a pool and add a custom strategy later
21+
22+
Both paths share the same smart-contract development steps (implementing `IStrategy` and `IStrategyFactory`).
23+
24+
## Smart contract development
25+
26+
1. Implement the [`IStrategy`](https://github.com/lidofinance/vaults-wrapper/blob/develop/src/interfaces/IStrategy.sol) interface
27+
28+
2. Implement the [`IStrategyFactory`](https://github.com/lidofinance/vaults-wrapper/blob/develop/src/interfaces/IStrategyFactory.sol) interface.
29+
The `_deployBytes` parameter can be used to pass additional strategy-specific configuration during deployment. If your strategy doesn't need extra config, it can be ignored.
30+
31+
3. Deploy the strategy factory
32+
33+
:::note
34+
Note the deployed **strategy factory address** — you will need it in Path A.
35+
:::
36+
37+
:::warning
38+
Make sure to deploy the strategy factory on the same network where you will create the pool (Hoodi testnet for testing, Ethereum mainnet for production).
39+
:::
40+
41+
---
42+
43+
## Path A: Deploy a new pool with custom strategy
44+
45+
Use this path when launching a new product from scratch.
46+
47+
### Create the pool via CLI
48+
49+
Use the `create-pool-custom` command to deploy the pool with your strategy:
50+
51+
```bash
52+
yarn start defi-wrapper contracts factory w create-pool-custom <DEFI_WRAPPER_FACTORY> \
53+
--nodeOperator <NODE_OPERATOR_ADDRESS> \
54+
--nodeOperatorManager <NODE_OPERATOR_MANAGER_ADDRESS> \
55+
--nodeOperatorFeeRateBP 10 \
56+
--confirmExpiry 86400 \
57+
--minDelaySeconds 3600 \
58+
--minWithdrawalDelayTime 3600 \
59+
--name "My Custom Strategy Pool" \
60+
--symbol STV \
61+
--proposer <PROPOSER_ADDRESS> \
62+
--executor <EXECUTOR_ADDRESS> \
63+
--emergencyCommittee <EMERGENCY_COMMITTEE_ADDRESS> \
64+
--reserveRatioGapBP 250 \
65+
--mintingEnabled true \
66+
--allowList true \
67+
--allowListManager <ALLOW_LIST_MANAGER_ADDRESS> \
68+
--strategyFactory <MY_STRATEGY_FACTORY_ADDRESS> \
69+
--strategyFactoryDeployBytes <strategyFactoryDeployBytes>
70+
```
71+
72+
Run `yarn start defi-wrapper contracts factory write create-pool-custom -h` for the full description of all available parameters.
73+
74+
:::info
75+
The deployer must have at least `1 ETH` available. This is the `CONNECT_DEPOSIT` required to be locked on the vault upon connection to Lido `VaultHub`.
76+
:::
77+
78+
<details>
79+
<summary>Parameter reference</summary>
80+
81+
| Parameter | Description |
82+
|-----------|-------------|
83+
| `<DEFI_WRAPPER_FACTORY>` | DeFi Wrapper Factory contract address (see [Environments](/run-on-lido/stvaults/building-guides/pooled-staking-product/#environments)) |
84+
| `--nodeOperator` | Address of the Node Operator managing validators |
85+
| `--nodeOperatorManager` | Address authorized to manage Node Operator settings |
86+
| `--nodeOperatorFeeRateBP` | Node Operator fee in basis points (10 = 0.1%) |
87+
| `--confirmExpiry` | Confirmation timeout in seconds |
88+
| `--minDelaySeconds` | TimeLock minimum delay before execution |
89+
| `--minWithdrawalDelayTime` | Minimum delay before withdrawals can be finalized |
90+
| `--name` | ERC-20 pool share token name |
91+
| `--symbol` | ERC-20 pool share token symbol |
92+
| `--proposer` | Address authorized to propose TimeLock operations |
93+
| `--executor` | Address authorized to execute TimeLock operations |
94+
| `--emergencyCommittee` | Address that can pause pool operations |
95+
| `--reserveRatioGapBP` | Reserve ratio gap in basis points (recommended min: 250) |
96+
| `--mintingEnabled` | Enable stETH minting (`true` / `false`) |
97+
| `--allowList` | Enable deposit allowlist (`true` / `false`) |
98+
| `--allowListManager` | Address managing the allowlist |
99+
| `--strategyFactory` | Your deployed strategy factory address |
100+
| `--strategyFactoryDeployBytes` | Optional hex-encoded bytes passed to your factory's `deploy()` |
101+
102+
</details>
103+
104+
:::warning
105+
The minimum recommended value for `reserveRatioGapBP` is `250` (2.5%). It is expected to be sufficient to absorb enough of the vault's performance volatility to keep users' positions healthy in most cases.
106+
:::
107+
108+
109+
After successful deployment, the CLI outputs the addresses and environment variables you need:
110+
111+
- **Vault** contract address
112+
- **Pool** contract address
113+
- **WithdrawalQueue** contract address
114+
- **Distributor** contract address
115+
- **Strategy** contract address
116+
- **TimeLock** contract address
117+
- UI environment variables (`VITE_POOL_ADDRESS`, `VITE_POOL_TYPE`, etc.)
118+
119+
:::info
120+
Keep the CLI output — you will need these addresses for the UI setup and ongoing operations.
121+
:::
122+
123+
Continue with [Post-deployment steps](/run-on-lido/stvaults/building-guides/pooled-staking-product/#2-create-web-ui).
124+
125+
---
126+
127+
## Path B: Upgrade an existing pool to a strategy pool
128+
129+
Use this path when you have a running [`StvStETHPool`](/run-on-lido/stvaults/building-guides/pooled-staking-product/#deployment-of-stvstethpool-pool-with-steth-minting) and want to add a strategy without redeploying the pool. All existing user balances and state are preserved through the proxy upgrade.
130+
131+
:::info
132+
This upgrade path uses the [`OssifiableProxy`](https://github.com/lidofinance/vaults-wrapper/blob/develop/src/proxy/OssifiableProxy.sol) pattern. The pool contract is a proxy whose implementation can be swapped by its admin (the `TimelockController`). Storage (user balances, roles, parameters) lives in the proxy and is preserved across implementation changes.
133+
:::
134+
135+
### What changes during the upgrade
136+
137+
| Aspect | Before (`StvStETHPool`) | After (`StvStrategyPool`) |
138+
|--------|------------------------|--------------------------------------|
139+
| Pool type | `STV_STETH_POOL_TYPE` | `STRATEGY_POOL_TYPE` |
140+
| Allowlist | Disabled | Enabled (only strategy can deposit) |
141+
| Strategy | None | Your custom strategy contract |
142+
| Direct user deposits | Allowed | Blocked (users go through strategy) |
143+
| User STV balances | ✅ Preserved | ✅ Preserved |
144+
| Vault, Dashboard, WQ | ✅ Unchanged | ✅ Unchanged |
145+
146+
### Deploy the new pool implementation and strategy
147+
148+
You need two new contracts: a new pool implementation (with `STRATEGY_POOL_TYPE` and `allowListEnabled = true`) and the strategy itself.
149+
150+
#### Deploy new pool implementation
151+
152+
Use the existing `StvStETHPoolFactory` to create a new implementation with the correct pool type:
153+
154+
```bash
155+
cast send <STV_STETH_POOL_FACTORY> \
156+
"deploy(address,bool,uint256,address,address,bytes32)(address)" \
157+
<DASHBOARD> \
158+
true \
159+
<RESERVE_RATIO_GAP_BP> \
160+
<WITHDRAWAL_QUEUE> \
161+
<DISTRIBUTOR> \
162+
<STRATEGY_POOL_TYPE> \
163+
--rpc-url $RPC_URL \
164+
--private-key $DEPLOYER_KEY
165+
```
166+
167+
Parameters:
168+
- `<STV_STETH_POOL_FACTORY>` — the `StvStETHPoolFactory` address from the DeFi Wrapper Factory (`Factory.STV_STETH_POOL_FACTORY()`)
169+
- `<DASHBOARD>` — your pool's existing Dashboard address
170+
- `true` — enables the allowlist (immutable in the new implementation)
171+
- `<RESERVE_RATIO_GAP_BP>` — same value as the existing pool (e.g., `500`)
172+
- `<WITHDRAWAL_QUEUE>` — your pool's existing WithdrawalQueue address
173+
- `<DISTRIBUTOR>` — your pool's existing Distributor address
174+
- `<STRATEGY_POOL_TYPE>` — the strategy pool type hash (`Factory.STRATEGY_POOL_TYPE()`)
175+
176+
Note the deployed **new pool implementation address**.
177+
178+
#### Deploy strategy implementation and proxy
179+
180+
181+
Deploy your strategy, you can use `forge create` or `cast send` for example
182+
183+
Note the deployed **strategy proxy address**.
184+
185+
:::warning
186+
The strategy proxy admin must be the pool's `TimelockController` address. The `initialize` call sets the Timelock as the strategy's `DEFAULT_ADMIN_ROLE` holder.
187+
:::
188+
189+
### Execute the upgrade via TimelockController batch
190+
191+
The upgrade must be executed as an **atomic batch** through the `TimelockController` to prevent an intermediate state where the allowlist is enabled but the strategy is not yet allowlisted.
192+
193+
The batch consists of operations, all targeting the pool proxy:
194+
195+
:::warning
196+
The exact number and content of operations depends on the current pool configuration (e.g., whether minting is paused, which roles are assigned). The example below is illustrative and may differ in your case.
197+
:::
198+
199+
| # | Operation | Purpose |
200+
|---|-----------|---------|
201+
| 1 | `proxy__upgradeToAndCall(newImpl, "")` | Swap implementation to strategy pool type |
202+
| 2 | `grantRole(ALLOW_LIST_MANAGER_ROLE, timelock)` | Temporarily grant allowlist management to Timelock |
203+
| 3 | `addToAllowList(strategyProxy)` | Allow the strategy to deposit into the pool |
204+
| 4 | `revokeRole(ALLOW_LIST_MANAGER_ROLE, factory)` | Remove Factory's allowlist management |
205+
| 5 | `revokeRole(ALLOW_LIST_MANAGER_ROLE, timelock)` | Remove Timelock's temporary allowlist management |
206+
| 6 | `revokeRole(DEPOSITS_PAUSE_ROLE, nodeOperator)` | Adjust pause roles for the new setup |
207+
| 7 | `revokeRole(MINTING_PAUSE_ROLE, nodeOperator)` | Adjust pause roles for the new setup |
208+
| 8 | `grantRole(MINTING_RESUME_ROLE, timelock)` | Temporarily grant minting resume capability |
209+
| 9 | `resumeMinting()` | Re-enable minting (needed if paused in the original pool) |
210+
| 10 | `revokeRole(MINTING_RESUME_ROLE, timelock)` | Remove temporary minting resume capability |
211+
212+
:::info
213+
Steps 8–10 (resume minting) are only needed if minting was paused in the original pool. If minting was already active, these steps can be omitted from the batch.
214+
:::
215+
216+
:::info
217+
Steps 6–7 (revoke pause roles from the Node Operator) adjust the emergency role setup to match the strategy pool configuration. Review the [DeFi Wrapper roles and permissions](./roles-and-permissions) to decide what role assignment is appropriate for your setup.
218+
:::
219+
220+
<details>
221+
<summary>Step 1: Prepare calldata for each operation</summary>
222+
223+
Use `cast` (from Foundry) to encode each payload:
224+
225+
```bash
226+
# 1. Upgrade pool implementation
227+
PAYLOAD_1=$(cast calldata "proxy__upgradeToAndCall(address,bytes)" <NEW_POOL_IMPL> 0x)
228+
229+
# 2. Grant ALLOW_LIST_MANAGER_ROLE to timelock
230+
ALLOW_LIST_MANAGER_ROLE=$(cast call <POOL> "ALLOW_LIST_MANAGER_ROLE()(bytes32)" --rpc-url $RPC_URL)
231+
PAYLOAD_2=$(cast calldata "grantRole(bytes32,address)" $ALLOW_LIST_MANAGER_ROLE <TIMELOCK>)
232+
233+
# 3. Add strategy to allowlist
234+
PAYLOAD_3=$(cast calldata "addToAllowList(address)" <STRATEGY_PROXY>)
235+
236+
# 4. Revoke ALLOW_LIST_MANAGER_ROLE from factory
237+
PAYLOAD_4=$(cast calldata "revokeRole(bytes32,address)" $ALLOW_LIST_MANAGER_ROLE <FACTORY>)
238+
239+
# 5. Revoke ALLOW_LIST_MANAGER_ROLE from timelock
240+
PAYLOAD_5=$(cast calldata "revokeRole(bytes32,address)" $ALLOW_LIST_MANAGER_ROLE <TIMELOCK>)
241+
242+
# 6. Revoke DEPOSITS_PAUSE_ROLE from node operator
243+
DEPOSITS_PAUSE_ROLE=$(cast call <POOL> "DEPOSITS_PAUSE_ROLE()(bytes32)" --rpc-url $RPC_URL)
244+
PAYLOAD_6=$(cast calldata "revokeRole(bytes32,address)" $DEPOSITS_PAUSE_ROLE <NODE_OPERATOR>)
245+
246+
# 7. Revoke MINTING_PAUSE_ROLE from node operator
247+
MINTING_PAUSE_ROLE=$(cast call <POOL> "MINTING_PAUSE_ROLE()(bytes32)" --rpc-url $RPC_URL)
248+
PAYLOAD_7=$(cast calldata "revokeRole(bytes32,address)" $MINTING_PAUSE_ROLE <NODE_OPERATOR>)
249+
250+
# 8. Grant MINTING_RESUME_ROLE to timelock
251+
MINTING_RESUME_ROLE=$(cast call <POOL> "MINTING_RESUME_ROLE()(bytes32)" --rpc-url $RPC_URL)
252+
PAYLOAD_8=$(cast calldata "grantRole(bytes32,address)" $MINTING_RESUME_ROLE <TIMELOCK>)
253+
254+
# 9. Resume minting
255+
PAYLOAD_9=$(cast calldata "resumeMinting()")
256+
257+
# 10. Revoke MINTING_RESUME_ROLE from timelock
258+
PAYLOAD_10=$(cast calldata "revokeRole(bytes32,address)" $MINTING_RESUME_ROLE <TIMELOCK>)
259+
```
260+
261+
</details>
262+
263+
<details>
264+
<summary>Step 2: Schedule the batch (Proposer)</summary>
265+
266+
Call `TimelockController.scheduleBatch` on the Timelock contract. This can be done via **Etherscan** or `cast`:
267+
268+
```bash
269+
POOL=<POOL_ADDRESS>
270+
PREDECESSOR=0x0000000000000000000000000000000000000000000000000000000000000000
271+
SALT=0x0000000000000000000000000000000000000000000000000000000000000000
272+
DELAY=<MIN_DELAY_SECONDS>
273+
274+
cast send <TIMELOCK> \
275+
"scheduleBatch(address[],uint256[],bytes[],bytes32,bytes32,uint256)" \
276+
"[$POOL,$POOL,$POOL,$POOL,$POOL,$POOL,$POOL,$POOL,$POOL,$POOL]" \
277+
"[0,0,0,0,0,0,0,0,0,0]" \
278+
"[$PAYLOAD_1,$PAYLOAD_2,$PAYLOAD_3,$PAYLOAD_4,$PAYLOAD_5,$PAYLOAD_6,$PAYLOAD_7,$PAYLOAD_8,$PAYLOAD_9,$PAYLOAD_10]" \
279+
$PREDECESSOR \
280+
$SALT \
281+
$DELAY \
282+
--rpc-url $RPC_URL \
283+
--private-key $PROPOSER_KEY
284+
```
285+
286+
Note the **operation ID** from the `CallScheduled` event in the transaction logs.
287+
288+
</details>
289+
290+
<details>
291+
<summary>Step 3: Execute the batch (Executor)</summary>
292+
293+
After the timelock delay has passed, execute the batch:
294+
295+
```bash
296+
cast send <TIMELOCK> \
297+
"executeBatch(address[],uint256[],bytes[],bytes32,bytes32)" \
298+
"[$POOL,$POOL,$POOL,$POOL,$POOL,$POOL,$POOL,$POOL,$POOL,$POOL]" \
299+
"[0,0,0,0,0,0,0,0,0,0]" \
300+
"[$PAYLOAD_1,$PAYLOAD_2,$PAYLOAD_3,$PAYLOAD_4,$PAYLOAD_5,$PAYLOAD_6,$PAYLOAD_7,$PAYLOAD_8,$PAYLOAD_9,$PAYLOAD_10]" \
301+
$PREDECESSOR \
302+
$SALT \
303+
--rpc-url $RPC_URL \
304+
--private-key $EXECUTOR_KEY
305+
```
306+
307+
You can verify the operation is ready before executing:
308+
```bash
309+
cast call <TIMELOCK> "isOperationReady(bytes32)(bool)" <OPERATION_ID> --rpc-url $RPC_URL
310+
```
311+
312+
</details>
313+
314+
### Verify the upgrade via CLI
315+
316+
```bash
317+
yarn start defi-wrapper contracts pool r info <POOL_ADDRESS>
318+
yarn start vo r info -v <VAULT_ADDRESS>
319+
```
320+
321+
### What users experience after the upgrade
322+
323+
- **Existing STV balances** are fully preserved — users keep their tokens.
324+
- **Direct deposits** to the pool are no longer possible (blocked by allowlist). Users must go through the strategy.
325+
- **Existing STV holders** can approve and deposit their tokens into the strategy to start receiving strategy-boosted yield.
326+
- **Withdrawals** of existing STV continue to work through the WithdrawalQueue as before.
327+
328+
---
329+
330+
## Reference implementation
331+
332+
The [`GGVStrategy`](https://github.com/lidofinance/vaults-wrapper/blob/develop/src/strategy/GGVStrategy.sol) and its [`GGVStrategyFactory`](https://github.com/lidofinance/vaults-wrapper/blob/develop/src/factories/GGVStrategyFactory.sol) serve as the reference implementation for custom strategies.
333+
334+
Study them to understand the complete pattern, including:
335+
336+
- How `StrategyCallForwarderRegistry` manages per-user proxies
337+
- How `FeaturePausable` enables granular pause control
338+
- How to handle ERC-20 approvals and transfers through call forwarders
339+
- How to implement cancel/replace flows for pending exit requests
340+
- How the proxy upgrade preserves all user state
341+
342+
The [upgrade integration test](https://github.com/lidofinance/vaults-wrapper/blob/develop/test/integration/wrapper-upgrade-b-to-c.test.sol) demonstrates the complete `StvStETHPool` → strategy pool upgrade flow.
343+
344+
## Useful links
345+
346+
- [DeFi Wrapper Technical Design](https://hackmd.io/@lido/lido-v3-wrapper-design)
347+
- [IStrategy interface](https://github.com/lidofinance/vaults-wrapper/blob/develop/src/interfaces/IStrategy.sol)
348+
- [IStrategyFactory interface](https://github.com/lidofinance/vaults-wrapper/blob/develop/src/interfaces/IStrategyFactory.sol)
349+
- [Upgrade integration test (StvStETHPool → strategy pool)](https://github.com/lidofinance/vaults-wrapper/blob/develop/test/integration/wrapper-upgrade-b-to-c.test.sol)
350+
- [stVaults CLI documentation](https://lidofinance.github.io/lido-staking-vault-cli/)
351+
- [stVaults Roles and Permissions](../../features-and-mechanics/roles-and-permissions)
352+
- [Health Monitoring Guide](../../operational-and-management-guides/health-monitoring-guide.md)

0 commit comments

Comments
 (0)