diff --git a/docusaurus.config.js b/docusaurus.config.js index 7f627222d..8bfec54df 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -201,7 +201,7 @@ module.exports = async function createConfigAsync() { from: '/run-on-lido/stvaults/operational-and-management-guides/stvault-disconnect-guide', }, { - to: '/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/redemptions_coverage_with_steth', + to: '/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/rebalance', from: '/run-on-lido/stvaults/operational-and-management-guides/voluntary-rebalancing-and-vault-closure', }, { diff --git a/run-on-lido/stvaults/builders/basic-stvaults/basic-isolated-staking-setup.md b/run-on-lido/stvaults/builders/basic-stvaults/basic-isolated-staking-setup.md index a8efacb35..d6d33c589 100644 --- a/run-on-lido/stvaults/builders/basic-stvaults/basic-isolated-staking-setup.md +++ b/run-on-lido/stvaults/builders/basic-stvaults/basic-isolated-staking-setup.md @@ -134,7 +134,7 @@ Creating an stVault is permissionless. There are two main ways to do it: 2. **Node Operator Manager address**. One of the two administrative roles in an stVault. From the Node Operator perspective, this role manages permissions and can update key vault parameters. Multiple addresses are supported. 3. **Vault Owner address**. One of the two administrative roles in an stVault. From the Vault Owner (Staker) perspective, this role manages permissions and can update key vault parameters. Multiple addresses are supported. 4. **Node Operator Fee**. The share of gross staking rewards that the Node Operator charges for providing validation services. Expressed in basis points [0 (0%) .. 10,000 (100%)]. -5. **Confirmation Lifetime**. The key parameter of the multi-role confirmation mechanism. It defines the maximum time interval between proposal and confirmation. This mechanism is used to update certain stVault parameters by requiring consensus between the two stVault representatives: the Vault Owner and the Node Operator Manager. Measured in seconds [86,400 sec (24 hours) .. 2,592,000 sec (30 days)]. For security reasons, it is strongly recommended to keep it as short as possible, ideally the minimum 86,400 sec. +5. **Confirmation Lifetime**. The key parameter of the multi-role confirmation mechanism. It defines the maximum time interval between proposal and confirmation. This mechanism is used to update certain stVault parameters by requiring consensus between the two stVault representatives: the Vault Owner and the Node Operator Manager. Measured in seconds [3,600 sec (1 hour) .. 2,592,000 sec (30 days)]. For security reasons, it is strongly recommended to keep it as short as possible. The CLI applies a stricter floor than the contract: whole hours, and at least 86,400 sec (24 hours) on Mainnet. ### 1. Two-step process (recommended) diff --git a/run-on-lido/stvaults/builders/basic-stvaults/leveraged-staking-product.md b/run-on-lido/stvaults/builders/basic-stvaults/leveraged-staking-product.md index 6e95fbe7a..6c1857ca9 100644 --- a/run-on-lido/stvaults/builders/basic-stvaults/leveraged-staking-product.md +++ b/run-on-lido/stvaults/builders/basic-stvaults/leveraged-staking-product.md @@ -179,7 +179,7 @@ Creating an stVault is permissionless. 2. **Node Operator Manager address**. One of the two administrative roles in an stVault. From the Node Operator perspective, this role manages permissions and can update key vault parameters. Multiple addresses are supported. 3. **Vault Owner address**. One of the two administrative roles in an stVault. From the Vault Owner (Staker) perspective, this role manages permissions and can update key vault parameters. Multiple addresses are supported. 4. **Node Operator Fee**. The share of gross staking rewards that the Node Operator charges for providing validation services. Expressed in basis points [0 (0%) .. 10,000 (100%)]. -5. **Confirmation Lifetime**. The key parameter of the multi-role confirmation mechanism. It defines the maximum time interval between proposal and confirmation. This mechanism is used to update certain stVault parameters by requiring consensus between the two stVault representatives: the Vault Owner and the Node Operator Manager. Measured in seconds [86,400 sec (24 hours) .. 2,592,000 sec (30 days)]. For security reasons, it is strongly recommended to keep it as short as possible, ideally the minimum 86,400 sec. +5. **Confirmation Lifetime**. The key parameter of the multi-role confirmation mechanism. It defines the maximum time interval between proposal and confirmation. This mechanism is used to update certain stVault parameters by requiring consensus between the two stVault representatives: the Vault Owner and the Node Operator Manager. Measured in seconds [3,600 sec (1 hour) .. 2,592,000 sec (30 days)]. For security reasons, it is strongly recommended to keep it as short as possible. The CLI applies a stricter floor than the contract: whole hours, and at least 86,400 sec (24 hours) on Mainnet. ## Creating new stVault diff --git a/run-on-lido/stvaults/builders/basic-stvaults/staking_with_redemptions_through_steth.md b/run-on-lido/stvaults/builders/basic-stvaults/staking_with_redemptions_through_steth.md index 14574f506..a653807f5 100644 --- a/run-on-lido/stvaults/builders/basic-stvaults/staking_with_redemptions_through_steth.md +++ b/run-on-lido/stvaults/builders/basic-stvaults/staking_with_redemptions_through_steth.md @@ -130,7 +130,7 @@ Creating an stVault is permissionless. 2. **Node Operator Manager address**. One of the two administrative roles in an stVault. From the Node Operator perspective, this role manages permissions and can update key vault parameters. Multiple addresses are supported. 3. **Vault Owner address**. One of the two administrative roles in an stVault. From the Vault Owner (Staker) perspective, this role manages permissions and can update key vault parameters. Multiple addresses are supported. 4. **Node Operator Fee**. The share of gross staking rewards that the Node Operator charges for providing validation services. Expressed in basis points [0 (0%) .. 10,000 (100%)]. -5. **Confirmation Lifetime**. The key parameter of the multi-role confirmation mechanism. It defines the maximum time interval between proposal and confirmation. This mechanism is used to update certain stVault parameters by requiring consensus between the two stVault representatives: the Vault Owner and the Node Operator Manager. Measured in seconds [86,400 sec (24 hours) .. 2,592,000 sec (30 days)]. For security reasons, it is strongly recommended to keep it as short as possible, ideally the minimum 86,400 sec. +5. **Confirmation Lifetime**. The key parameter of the multi-role confirmation mechanism. It defines the maximum time interval between proposal and confirmation. This mechanism is used to update certain stVault parameters by requiring consensus between the two stVault representatives: the Vault Owner and the Node Operator Manager. Measured in seconds [3,600 sec (1 hour) .. 2,592,000 sec (30 days)]. For security reasons, it is strongly recommended to keep it as short as possible. The CLI applies a stricter floor than the contract: whole hours, and at least 86,400 sec (24 hours) on Mainnet. ## Creating new stVault diff --git a/run-on-lido/stvaults/builders/defi-wrapper/multi-user-delegated-staking.md b/run-on-lido/stvaults/builders/defi-wrapper/multi-user-delegated-staking.md index ed9be215d..b84e72ddf 100644 --- a/run-on-lido/stvaults/builders/defi-wrapper/multi-user-delegated-staking.md +++ b/run-on-lido/stvaults/builders/defi-wrapper/multi-user-delegated-staking.md @@ -42,7 +42,7 @@ This guide walks through the concepts and practical steps to launch such a produ ### 1. Create a tokenized staking vault (pool) The easiest way to create a tokenized staking vault (pool) is to use the [stVaults CLI](https://lidofinance.github.io/lido-staking-vault-cli). -It's a command-line tool for managing both staking vaults and DeFi Wrapper pools. It deploys a pool plus its underlying staking vault via the [`Factory`](https://github.com/lidofinance/vaults-wrapper/blob/develop/src/Factory.sol) contract. +It's a command-line tool for managing both staking vaults and DeFi Wrapper pools. It deploys a pool plus its underlying staking vault via the [`Factory`](https://github.com/lidofinance/vaults-wrapper/blob/main/src/Factory.sol) contract. The CLI performs the deployment in two transactions to stay within the current 16M transaction gas limit. @@ -125,7 +125,7 @@ The minimum recommended value for `reserveRatioGapBP` is `250` (2.5%). It is exp ### 2. Create Web UI -Follow this [guide](https://github.com/lidofinance/defi-wrapper-widget/blob/develop/README.md) to: +Follow this [guide](https://github.com/lidofinance/defi-wrapper-widget/blob/main/README.md) to: - Clone the provided repository - Use addresses outputted by CLI to fill up `.env` @@ -144,7 +144,7 @@ Thus, changing tier for a pooled vault is a three-step process: 1. Holder of the Timelock's proposer role calls `TimelockController.schedule` to propose the `Dashboard.changeTier` call 2. After the timelock period, the holder of the Timelock's executor role calls `TimelockController.execute` for the scheduled proposal -3. Within the confirmation time window period (24 hours at the Mainnet minimum), the Node Operator confirms from their side by calling `OperatorGrid.changeTier(vault, tierId, requestedShareLimit)` — the same tier and share limit, but through a different contract and with the stVault as an extra argument +3. Within the `OperatorGrid` confirmation expiry (currently 24 hours), the Node Operator confirms from their side by calling `OperatorGrid.changeTier(vault, tierId, requestedShareLimit)` — the same tier and share limit, but through a different contract and with the stVault as an extra argument Confirming tier change request requires applying fresh report to vault. [Read more about applying reports](../../vault-owners-curators-and-stakers/basic-stvaults/apply-oracle-reports.md) @@ -277,7 +277,7 @@ Use `--wallet-connect` option for all commands or provide private key to CLI `.e
Step 3: Confirm the tier change (Node Operator) -Within the confirmation time window period (24 hours at the Mainnet minimum) after step 2, the Node Operator must confirm the tier change: +Within the `OperatorGrid` confirmation expiry (currently 24 hours) after step 2, the Node Operator must confirm the tier change: #### stVaults UI diff --git a/run-on-lido/stvaults/builders/defi-wrapper/multi-user-staking-with-custom-strategy.md b/run-on-lido/stvaults/builders/defi-wrapper/multi-user-staking-with-custom-strategy.md index c4213b2a5..f5c73e689 100644 --- a/run-on-lido/stvaults/builders/defi-wrapper/multi-user-staking-with-custom-strategy.md +++ b/run-on-lido/stvaults/builders/defi-wrapper/multi-user-staking-with-custom-strategy.md @@ -85,7 +85,7 @@ yarn start defi-wrapper contracts factory w create-pool-custom \ --executor \ @@ -120,8 +120,8 @@ The deployer must have at least `1 ETH` available. This is the `CONNECT_DEPOSIT` | `--confirmExpiry` | Confirmation timeout in seconds | | `--minDelaySeconds` | TimeLock minimum delay before execution | | `--minWithdrawalDelayTime` | Minimum delay before withdrawals can be finalized | -| `--name` | ERC-20 pool share token name | -| `--symbol` | ERC-20 pool share token symbol | +| `--name` | ERC-20 pool share token name; the CLI enforces 3–14 characters | +| `--symbol` | ERC-20 pool share token symbol; the CLI enforces 3–8 characters | | `--proposer` | Address authorized to propose TimeLock operations | | `--executor` | Address authorized to execute TimeLock operations | | `--emergencyCommittee` | Address that can pause pool operations | @@ -141,13 +141,13 @@ The minimum recommended value for `reserveRatioGapBP` is `250` (2.5%). It is exp After successful deployment, the CLI outputs the addresses and environment variables you need: -- **Vault** contract address -- **Pool** contract address -- **WithdrawalQueue** contract address -- **Distributor** contract address -- **Strategy** contract address -- **TimeLock** contract address -- UI environment variables (`VITE_POOL_ADDRESS`, `VITE_POOL_TYPE`, etc.) +The first transaction prints the **Dashboard**, **Pool Proxy**, **Withdrawal Queue Proxy** and **TimeLock**; +the second adds the **Vault**, **Pool**, **Pool Type**, **Withdrawal Queue**, **Strategy Factory** and +**Strategy**, along with the UI environment variables (`VITE_POOL_ADDRESS`, `VITE_POOL_TYPE`). + +The **Distributor** address is not printed. Read it later with `yarn start dw uc wo r info `, +which returns the whole set — see +[Per-setup addresses](../../concepts-and-reference/architecture-overview.md#per-setup-addresses). :::info Keep the CLI output — you will need these addresses for the UI setup and ongoing operations. @@ -201,7 +201,7 @@ Parameters: - `` — the `StvStETHPoolFactory` address from the DeFi Wrapper Factory (`Factory.STV_STETH_POOL_FACTORY()`) - `` — your pool's existing Dashboard address - `true` — enables the allowlist (immutable in the new implementation) -- `` — same value as the existing pool (e.g., `500`) +- `` — the existing pool's value, readable with `poolReserveRatioBP` minus the stVault's ratio; `250` in the shipped configurations - `` — your pool's existing WithdrawalQueue address - `` — your pool's existing Distributor address - `` — the strategy pool type hash (`Factory.STRATEGY_POOL_TYPE()`) @@ -238,7 +238,7 @@ INITIALIZE_CALLDATA=$(cast calldata "initialize(address,address)" ` — the pool's TimelockController address (will receive `DEFAULT_ADMIN_ROLE` on the strategy) -- `` — address that will receive the initial pause role (e.g., `SUPPLY_PAUSE_ROLE`) +- `` — address that receives `SUPPLY_PAUSE_ROLE`; pass the zero address to grant it to nobody Then deploy the proxy: @@ -271,7 +271,7 @@ The exact number and content of operations depends on the current pool configura | 1 | `proxy__upgradeToAndCall(newImpl, "")` | Swap implementation to strategy pool type | | 2 | `grantRole(ALLOW_LIST_MANAGER_ROLE, timelock)` | Temporarily grant allowlist management to Timelock | | 3 | `addToAllowList(strategyProxy)` | Allow the strategy to deposit into the pool | -| 4 | `revokeRole(ALLOW_LIST_MANAGER_ROLE, factory)` | Remove Factory's allowlist management | +| 4 | `revokeRole(ALLOW_LIST_MANAGER_ROLE, factory)` | Remove the Factory's allowlist management. Not optional: the Factory has held this role since the pool was created, and the upgrade is what makes it usable | | 5 | `revokeRole(ALLOW_LIST_MANAGER_ROLE, timelock)` | Remove Timelock's temporary allowlist management | | 6 | `revokeRole(DEPOSITS_PAUSE_ROLE, nodeOperator)` | Adjust pause roles for the new setup | | 7 | `revokeRole(MINTING_PAUSE_ROLE, nodeOperator)` | Adjust pause roles for the new setup | @@ -392,7 +392,7 @@ yarn start vo r info -v - **Existing STV balances** are fully preserved — users keep their tokens. - **Direct deposits** to the pool are no longer possible (blocked by allowlist). Users must go through the strategy. -- **Existing STV holders** can approve and deposit their tokens into the strategy to start receiving strategy-boosted yield. +- **Existing STV holders** can move into the strategy, but not by approving it: the strategy has no function that pulls stv from a user's address. `supply` either takes ETH or mints against stv the user's forwarder already holds. To migrate, transfer the stv to that forwarder — its address is deterministic and readable with `getStrategyCallForwarderAddress(user)` — and then call `supply` with a non-zero wstETH amount. - **Withdrawals** of existing STV continue to work through the WithdrawalQueue as before. --- @@ -434,7 +434,7 @@ Thus, changing tier for a pooled vault is a three-step process: 1. Holder of the Timelock's proposer role calls `TimelockController.schedule` to propose the `Dashboard.changeTier` call 2. After the timelock period, the holder of the Timelock's executor role calls `TimelockController.execute` for the scheduled proposal -3. Within the confirmation time window period (24 hours at the Mainnet minimum), the Node Operator confirms from their side by calling `OperatorGrid.changeTier(vault, tierId, requestedShareLimit)` — the same tier and share limit, but through a different contract and with the stVault as an extra argument +3. Within the `OperatorGrid` confirmation expiry (currently 24 hours), the Node Operator confirms from their side by calling `OperatorGrid.changeTier(vault, tierId, requestedShareLimit)` — the same tier and share limit, but through a different contract and with the stVault as an extra argument Confirming tier change request requires applying fresh report to vault. [Read more about applying reports](../../vault-owners-curators-and-stakers/basic-stvaults/apply-oracle-reports.md) @@ -567,7 +567,7 @@ Use `--wallet-connect` option for all commands or provide private key to CLI `.e
Step 3: Confirm the tier change (Node Operator) -Within the confirmation time window period (24 hours at the Mainnet minimum) after step 2, the Node Operator must confirm the tier change: +Within the `OperatorGrid` confirmation expiry (currently 24 hours) after step 2, the Node Operator must confirm the tier change: #### stVaults UI diff --git a/run-on-lido/stvaults/builders/defi-wrapper/multi-user-staking-with-earn-eth.md b/run-on-lido/stvaults/builders/defi-wrapper/multi-user-staking-with-earn-eth.md index 03b32d06f..6b74d705a 100644 --- a/run-on-lido/stvaults/builders/defi-wrapper/multi-user-staking-with-earn-eth.md +++ b/run-on-lido/stvaults/builders/defi-wrapper/multi-user-staking-with-earn-eth.md @@ -54,7 +54,7 @@ To start: :::info -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`. +The deployer must have at least `1 ETH` available. This is the `CONNECT_DEPOSIT` required to be locked on the stVault upon connection to Lido `VaultHub`. The newly created staking vault is automatically connected to Lido `VaultHub` and placed into the default tier. Placement into non-default tiers right upon deployment is not supported. @@ -75,7 +75,7 @@ Keep this output if you plan to set up the UI. #### Deployment of `StvStrategyPool` with the `Lido Earn ETH` strategy -The pool with the `Lido Earn ETH` strategy: ETH is deposited to validators and generates staking rewards, stETH is minted and automatically deposited to the Earn ETH strategy to earn additional rewards. Deposited stETH is distributed across a curated set of high-performing DeFi strategies, including lending markets and LP positions. The `Earn ETH` strategy is built on Mellow architecture, so the strategy connector is called "MellowStrategy", and the factory is called "MellowStrategyFactory". +The pool with the `Lido Earn ETH` strategy: ETH is deposited to validators and generates staking rewards, stETH is minted, wrapped into wstETH and automatically deposited to the Earn ETH strategy to earn additional rewards. Deposited wstETH is distributed across a curated set of high-performing DeFi strategies, including lending markets and LP positions. The `Earn ETH` strategy is built on Mellow architecture, so the strategy connector is called "MellowStrategy", and the factory is called "MellowStrategyFactory". To deploy this pool, use the `create-strategy-pool-lido-earn-eth`. The factory addresses for each network are listed in the [Environments](../../concepts-and-reference/architecture-overview#environments) section. The full parameter reference is available below. @@ -89,7 +89,7 @@ yarn start defi-wrapper contracts factory w create-strategy-pool-lido-earn-eth < --confirmExpiry 86400 \ --minDelaySeconds 3600 \ --minWithdrawalDelayTime 3600 \ - --name "Staked Earn ETH Pool" \ + --name "Earn ETH Pool" \ --symbol STV \ --proposer \ --executor \ @@ -100,7 +100,7 @@ yarn start defi-wrapper contracts factory w create-strategy-pool-lido-earn-eth < You can use `--allowList true` to enable the deposit allowlist for this strategy. AllowList Manager role on Strategy must be set separately by TimeLock governance. :::warning -AllowList will be always enabled on StvStethPool contract. This allowlist ensures only the strategy contract can deposit into the pool, and minting is required to produce wstETH for the Earn ETH Vault. Strategy contract has its own allow list. +AllowList will be always enabled on the `StvStETHPool` contract. This allowlist ensures only the strategy contract can deposit into the pool, and minting is required to produce wstETH for the Earn ETH Vault. Strategy contract has its own allow list. :::
@@ -113,11 +113,11 @@ AllowList will be always enabled on StvStethPool contract. This allowlist ensure | `--nodeOperator` | Address of the Node Operator managing validators | | `--nodeOperatorManager` | Address authorized to manage Node Operator settings | | `--nodeOperatorFeeRateBP` | Node Operator fee in basis points (10 = 0.1%) | -| `--confirmExpiry` | Confirmation timeout in seconds | -| `--minDelaySeconds` | TimeLock minimum delay before execution | -| `--minWithdrawalDelayTime` | Minimum delay before withdrawals can be finalized | -| `--name` | ERC-20 pool share token name | -| `--symbol` | ERC-20 pool share token symbol | +| `--confirmExpiry` | Confirmation timeout in seconds (whole hours; min 24 hours on Mainnet, 1 hour on testnets, max 30 days) | +| `--minDelaySeconds` | TimeLock minimum delay before execution (whole hours, max 30 days) | +| `--minWithdrawalDelayTime` | Minimum delay before withdrawals can be finalized (whole hours, min 1 hour, max 30 days) | +| `--name` | ERC-20 pool share token name (3–14 characters) | +| `--symbol` | ERC-20 pool share token symbol (3–8 characters) | | `--proposer` | Address authorized to propose TimeLock operations | | `--executor` | Address authorized to execute TimeLock operations | | `--emergencyCommittee` | Address that can pause pool operations | @@ -127,7 +127,7 @@ AllowList will be always enabled on StvStethPool contract. This allowlist ensure #### Managing AllowList for `StvStrategyPool` with `Lido Earn ETH` strategy -Due to design the allow list for `StvStrategyPool` is always on and is limited only to the strategies contracts attached to the pool. The strategy contract(if enabled by `--allowList true`) has it's own allow list. To manage the Strategy allow list, use the following CLI commands: +Due to design the allow list for `StvStrategyPool` is always on and is limited only to the strategies contracts attached to the pool. The strategy contract(if enabled by `--allowList true`) has its own allow list. To manage the Strategy allow list, use the following CLI commands: - `yarn start defi-wrapper use-cases wrapper-operations read info ` to check the current strategy address attached to the pool - `yarn start defi-wrapper use-cases timelock-governance common read get-timelock-address ` to get the timelock address for the pool @@ -159,7 +159,7 @@ Thus, changing tier for a pooled vault is a three-step process: 1. Holder of the Timelock's proposer role calls `TimelockController.schedule` to propose the `Dashboard.changeTier` call 2. After the timelock period, the holder of the Timelock's executor role calls `TimelockController.execute` for the scheduled proposal -3. Within the confirmation time window period (24 hours at the Mainnet minimum), the Node Operator confirms from their side by calling `OperatorGrid.changeTier(vault, tierId, requestedShareLimit)` — the same tier and share limit, but through a different contract and with the vault as an extra argument +3. Within the `OperatorGrid` confirmation expiry (currently 24 hours), the Node Operator confirms from their side by calling `OperatorGrid.changeTier(vault, tierId, requestedShareLimit)` — the same tier and share limit, but through a different contract and with the stVault as an extra argument Confirming tier change request requires applying fresh report to vault. [Read more about applying reports](../../vault-owners-curators-and-stakers/basic-stvaults/apply-oracle-reports.md) @@ -292,7 +292,7 @@ Use `--wallet-connect` option for all commands or provide private key to CLI `.e
Step 3: Confirm the tier change (Node Operator) -Within the confirmation time window period (24 hours at the Mainnet minimum) after step 2, the Node Operator must confirm the tier change: +Within the `OperatorGrid` confirmation expiry (currently 24 hours) after step 2, the Node Operator must confirm the tier change: #### stVaults UI diff --git a/run-on-lido/stvaults/concepts-and-reference/defi-wrapper-technical-design.md b/run-on-lido/stvaults/concepts-and-reference/defi-wrapper-technical-design.md index 4a1a80a27..a11734384 100644 --- a/run-on-lido/stvaults/concepts-and-reference/defi-wrapper-technical-design.md +++ b/run-on-lido/stvaults/concepts-and-reference/defi-wrapper-technical-design.md @@ -11,8 +11,6 @@ sidebar_label: DeFi Wrapper Technical Design The DeFi Wrapper turns a single [stVault](./stvaults-technical-design.md) into a multi-user product. An stVault on its own has one owner; the Wrapper puts a tokenized pool in front of it, so many depositors can share one vault, hold a transferable claim on it, mint stETH against their own share, and route that stETH into a DeFi strategy — without any of them holding vault permissions. -Everything is deployed from a factory in two transactions and handed to a timelock. The Vault Owner keeps the levers that matter for safety (pause, upgrade) and gives up the ones that would let them touch user funds. - ## 2. Design ### 2.1 Goals @@ -20,11 +18,11 @@ Everything is deployed from a factory in two transactions and handed to a timelo - **Pool one stVault across many depositors** while keeping each depositor's position individually accounted. - **Keep stETH minting per-account.** A depositor's debt is their own: it limits their own withdrawals and can be rebalanced away without touching anyone else's position. - **Make the operator's discretion bounded.** The Node Operator decides *when* to return ETH from validators, but not at what rate a request settles. -- **Deploy without a trusted setup step.** The factory wires every role in one transaction and revokes itself, so a deployment cannot be left half-configured. +- **Deploy without a trusted setup step.** Until control passes to the timelock, the pool's admin is the factory itself, fixed code rather than a person. ### 2.2 Principles -- **The pool holds no permissions a user could abuse.** Vault roles land on the pool, the queue and the timelock — never on an individual. +- **No individual holds an stVault role that moves funds.** Those roles go to the pool and the queue as contracts, with admin on the timelock. - **Losses are shared, gains are not.** A withdrawal request settles at the *lower* of its creation rate and the finalization rate, so waiting in the queue cannot be used to capture rewards, and cannot be used to escape penalties. - **Degrade automatically, recover deliberately.** Bad debt, unassigned liability and stale reports block operations without anyone acting; unpausing after an incident requires the timelock. - **Stay inside Lido Core's guarantees.** The Wrapper never re-implements vault accounting; it reads `maxLockableValue`, `liabilityShares` and report freshness from the Dashboard and VaultHub. @@ -43,9 +41,9 @@ Only two of these are contracts. A strategy pool is `StvStETHPool` carrying the ### 2.4 Lido fee socialization and economic consequences -Nothing stops a minting pool from holding both depositors who mint stETH and depositors who only stake — minting is per account, and neither the pool nor the vault rejects the mix. It is left unsupported because of how the cost lands. +Nothing stops a minting pool from holding both depositors who mint stETH and depositors who only stake — minting is per account, and neither the pool nor the stVault rejects the mix. It is left unsupported because of how the cost lands. -The liquidity and reservation fees are charged on the vault as a whole. They raise its cumulative Lido fees, which lowers `maxLockableValue`, which lowers `totalAssets()` — so the price of every stv drops, including that of holders who never minted. LazyOracle reports for the vault, not per account, so there is no way to bill those fees back to the accounts that caused them. +The liquidity and reservation fees are charged on the stVault as a whole. They raise its cumulative Lido fees, which lowers `maxLockableValue`, which lowers `totalAssets()` — so the price of every stv drops, including that of holders who never minted. LazyOracle reports for the stVault, not per account, so there is no way to bill those fees back to the accounts that caused them. The result is a silent subsidy from stakers to minters, which is why the recommended shape is one pool per behaviour. @@ -70,7 +68,7 @@ The pool never holds vault ownership. The factory grants `FUND_ROLE`, `REBALANCE ### 3.2 StvPool -The base pool. It accepts ETH, forwards it into the stVault through `Dashboard.fund()`, and issues `stv` — a transferable ERC-20 claim on the vault's value. +The base pool. It accepts ETH, forwards it into the stVault through `Dashboard.fund()`, and issues `stv` — a transferable ERC-20 claim on the stVault's value. :::info **stv** stands for *staking vault token* — the pool's own share token. @@ -78,7 +76,7 @@ The base pool. It accepts ETH, forwards it into the stVault through `Dashboard.f #### stv accounting -`stv` has **27 decimals** while the underlying asset has 18. The pool is initialized by minting `vaultBalance × 1e9` stv **to itself** for the connect deposit already sitting in the vault, which fixes the starting rate at 1 ETH = 1e27 stv and keeps later conversions exact. +`stv` has **27 decimals** while the underlying asset has 18. The pool is initialized by minting `vaultBalance × 1e9` stv **to itself** for the connect deposit already sitting in the stVault, which fixes the starting rate at 1 ETH = 1e27 stv and keeps later conversions exact. The value backing the token is read from Lido Core, not tracked locally: @@ -96,8 +94,8 @@ Conversions round in the direction that protects the pool: `previewDeposit` floo Two conditions can arise that make the pool's own accounting untrustworthy, and both freeze it: -- **Unassigned liability** — the vault owes stETH that no pool account is recorded as owing, measured as the excess of vault liability over the pool's recorded minted shares. It arises through [bad debt socialization](./stvaults-technical-design.md#bad-debt): the DAO can move uncovered liability from one vault onto another **run by the same Node Operator**, and if the pool's vault is the acceptor, its liability grows while nobody in the pool has minted anything. -- **Bad debt** — the vault owes more stETH than it is worth, so there is no longer enough value behind stv to price it. This is not a normal state: losses have to exceed the reserve entirely, which takes an exceptional event such as mass slashing, not a dip in validator performance. Rebalancing cannot fix it; the Lido Core [escalation path](./stvaults-technical-design.md#bad-debt) can. +- **Unassigned liability** — the stVault owes stETH that no pool account is recorded as owing, measured as the excess of vault liability over the pool's recorded minted shares. It arises through [bad debt socialization](./stvaults-technical-design.md#bad-debt): the DAO can move uncovered liability from one vault onto another **run by the same Node Operator**, and if the pool's vault is the acceptor, its liability grows while nobody in the pool has minted anything. +- **Bad debt** — the stVault owes more stETH than it is worth, so there is no longer enough value behind stv to price it. This is not a normal state: losses have to exceed the reserve entirely, which takes an exceptional event such as mass slashing, not a dip in validator performance. Rebalancing cannot fix it; the Lido Core [escalation path](./stvaults-technical-design.md#bad-debt) can. Both are checked inside the ERC-20 `_update` hook, so while either holds, **every** transfer, mint and burn of stv reverts — deposits included. No role is involved and nobody can override it; the condition has to be cleared. @@ -108,7 +106,7 @@ function rebalanceUnassignedLiability(uint256 _stethShares) external; function rebalanceUnassignedLiabilityWithEther() external payable; ``` -The first repays it out of the vault's own assets, the second out of ETH the caller supplies. Neither can repay more than the unassigned amount, or the call reverts with `NotEnoughToRebalance`. That cap matters because both spend the vault's assets, which belong to every stv holder: without it, anyone could clear one account's personal debt at everyone else's expense. +The first repays it out of the stVault's own assets, the second out of ETH the caller supplies. Neither can repay more than the unassigned amount, or the call reverts with `NotEnoughToRebalance`. That cap matters because both spend the stVault's assets, which belong to every stv holder: without it, anyone could clear one account's personal debt at everyone else's expense. #### Deposits @@ -127,7 +125,7 @@ Adds per-account stETH minting on top of `StvPool`. Each account has its own deb #### The reserve ratio gap -The pool does **not** mint up to the vault's own reserve ratio. It keeps a margin: +The pool does **not** mint up to the stVault's own reserve ratio. It keeps a margin: $$ RR_{\text{pool}} = RR_{\text{vault}} + \text{gap} @@ -139,7 +137,7 @@ The gap is immutable per deployment and is **250 BP (2.5%)** in every shipped co Both results are capped just below 100%: a reserve ratio of exactly 100% would leave nothing to mint against and would divide by zero in the collateral formulas, and the threshold is capped one basis point lower still so that it stays under the reserve ratio. Neither cap binds in practice, since the highest vault reserve ratio is the Default tier's 50%. -The pool keeps its own copy of both numbers rather than reading them from the vault on each call, so a tier change in Lido Core does not reach it by itself: until someone calls `syncVaultParameters()`, minting capacity and the rebalancing threshold still follow the old tier. The call is permissionless, so anyone can bring them up to date. +The pool keeps its own copy of both numbers rather than reading them from the stVault on each call, so a tier change in Lido Core does not reach it by itself: until someone calls `syncVaultParameters()`, minting capacity and the rebalancing threshold still follow the old tier. The call is permissionless, so anyone can bring them up to date. #### Per-account collateral @@ -184,14 +182,14 @@ If the account's stv does not cover its debt, the account is **undercollateraliz #### Exceeding minted stETH -Two contracts count the same debt. The pool tracks what its accounts owe *it*, and the vault tracks what it owes *Lido Core*. Normally the two agree; when they drift apart, the difference has a name in each direction: +Two contracts count the same debt. The pool tracks what its accounts owe *it*, and the stVault tracks what it owes *Lido Core*. Normally the two agree; when they drift apart, the difference has a name in each direction: -- the vault owes more than the pool has on record → **unassigned liability**, covered in [§3.2](#32-stvpool); -- the pool has on record more than the vault owes → **exceeding minted stETH**. +- the stVault owes more than the pool has on record → **unassigned liability**, covered in [§3.2](#32-stvpool); +- the pool has on record more than the stVault owes → **exceeding minted stETH**. -The second happens when the vault's debt is repaid without the pool being involved — a rebalance performed on the vault directly. The vault spends its own ETH to burn stETH liability, so both its value and its liability fall, while every account in the pool still owes exactly what it owed before. +The second happens when the stVault's debt is repaid without the pool being involved — a rebalance performed on the stVault directly. The stVault spends its own ETH to burn stETH liability, so both its value and its liability fall, while every account in the pool still owes exactly what it owed before. -Those unchanged debts are worth something. The accounts owe stETH that the vault no longer owes anyone, and that claim belongs to the pool, offsetting the ETH the rebalance consumed. So the pool can hold value in two forms at once, and `totalAssets()` picks the branch that applies: +Those unchanged debts are worth something. The accounts owe stETH that the stVault no longer owes anyone, and that claim belongs to the pool, offsetting the ETH the rebalance consumed. So the pool can hold value in two forms at once, and `totalAssets()` picks the branch that applies: ``` exceeding > 0 → totalAssets = nominalAssets + exceedingMintedSteth @@ -200,7 +198,7 @@ otherwise → totalAssets = nominalAssets − unassignedLiabilitySteth Only one branch can ever be live, because the two quantities are the same difference measured in opposite directions. -An account can settle against that claim with `rebalanceExceedingMintedStethShares`: it burns its own stv, its debt drops, and no vault-level rebalance is needed — the vault's liability is already lower. The catch is that the exceeding amount is one pool-wide budget served first come, first served. The contract's NatSpec flags the front-running risk outright, and whoever loses the race gets `InsufficientExceedingShares`. +An account can settle against that claim with `rebalanceExceedingMintedStethShares`: it burns its own stv, its debt drops, and no vault-level rebalance is needed — the stVault's liability is already lower. The catch is that the exceeding amount is one pool-wide budget served first come, first served. The contract's NatSpec flags the front-running risk outright, and whoever loses the race gets `InsufficientExceedingShares`. ### 3.4 WithdrawalQueue @@ -234,8 +232,8 @@ function finalize(uint256 _maxRequests, address _gasCostCoverageRecipient) exter `FINALIZE_ROLE` only, held by the Node Operator by default. The call walks the queue from the first unfinalized request and **stops at the first request that fails any of four conditions**: -1. claimable ETH exceeds the vault's `withdrawableValue`; -2. claimable plus rebalanced ETH exceeds the vault's `availableBalance`; +1. claimable ETH exceeds the stVault's `withdrawableValue`; +2. claimable plus rebalanced ETH exceeds the stVault's `availableBalance`; 3. the minimum withdrawal delay has not elapsed since the request was created; 4. the request was created *after* the latest oracle report — at least one report must have landed in between. @@ -264,7 +262,7 @@ The operator cannot set the rate. What they do choose is *when* to finalize and #### What a claim pays out -A request can carry stETH debt as well as stv, through `stethSharesToRebalance`. Finalization settles that debt out of the vault and burns the stv that backed it, so only the remainder leaves as ETH: +A request can carry stETH debt as well as stv, through `stethSharesToRebalance`. Finalization settles that debt out of the stVault and burns the stv that backed it, so only the remainder leaves as ETH: ``` payout = assets (discounted to the checkpoint rate, if the rate fell) @@ -282,7 +280,7 @@ Finalization is work the Node Operator pays for while the exiting depositors get The amount is set by `FINALIZE_ROLE` through `setFinalizationGasCostCoverage`, is **0 by default**, and cannot exceed `MAX_GAS_COST_COVERAGE`, a constant of 0.0005 ETH per request. The ceiling is what stops an operator from turning the deduction into a toll on exits. -At finalization each request gives up `min(payout, coverage)` — a request worth less than the coverage surrenders what it has and never goes negative — and the total is withdrawn from the vault alongside the claimable ETH and sent to the address passed to `finalize`, defaulting to the caller. +At finalization each request gives up `min(payout, coverage)` — a request worth less than the coverage surrenders what it has and never goes negative — and the total is withdrawn from the stVault alongside the claimable ETH and sent to the address passed to `finalize`, defaulting to the caller. The rate in force is captured in the checkpoint, so an operator who changes it later does not re-price requests that were already finalized but not yet claimed. @@ -321,14 +319,14 @@ EarnETH strategy talks to a Mellow vault through three queues: a synchronous dep **Entering** picks a path with `MellowSupplyParams{isSync, merkleProof}`. The proof is Mellow's whitelist check, run against the user's forwarder — without it the supply is refused. The two paths differ in cost as much as in timing: -- **Synchronous** settles in the same transaction, and costs more for it: the price is cut by the queue's penalty and then by the vault's deposit fee. It also only works while Mellow's price report is younger than the queue's `maxAge`. +- **Synchronous** settles in the same transaction, and costs more for it: the price is cut by the queue's penalty and then by the Mellow vault's deposit fee. It also only works while Mellow's price report is younger than the queue's `maxAge`. - **Asynchronous** records a request the user collects later with `claimShares()`, paying the deposit fee but no penalty. Only one request may be outstanding at a time — supplying again before claiming is rejected. Either path is simulated by `previewSupply` first, and `supply` reverts with `SupplyFailed()` if that simulation fails: a paused queue, a stale or suspicious price, a missing whitelist entry all stop the deposit before any ETH moves. **Leaving is always asynchronous.** `requestExitByWsteth` places a `redeem` on the redeem queue, and the position is collected later with `finalizeRequestExit(requestId)`. The request id is `bytes32(block.timestamp)`, so a user's exits made in the same block merge into one underlying request: expect more than one event carrying that id, and a single finalize settling the lot. -The constructor validates the queues against the vault before anything is deployed — each must belong to that vault, be of the right kind, and hold wstETH as its asset, with the synchronous one additionally named `SyncDepositQueue`. It also requires that the strategy itself has no pre-existing deposit or redeem request. A mismatch reverts with `InvalidQueue`, so an adapter cannot be pointed at a Mellow vault it does not fit. +The constructor validates the queues against the Mellow vault before anything is deployed — each must belong to that vault, be of the right kind, and hold wstETH as its asset, with the synchronous one additionally named `SyncDepositQueue`. It also requires that the strategy itself has no pre-existing deposit or redeem request. A mismatch reverts with `InvalidQueue`, so an adapter cannot be pointed at a Mellow vault it does not fit. ### 3.6 Distributor @@ -342,23 +340,23 @@ function claim(address _recipient, address _token, uint256 _cumulativeAmount, by Accounting is cumulative: the leaf commits to a total, and a claim transfers the difference against what that recipient already took. A root can be set at most once per block and must actually change. -`claim` is **permissionless** — anyone may submit a proof on someone's behalf, and the tokens always go to the recipient named in the leaf. It works from the DeFi Wrapper widget or the CLI; for strategy pools the leaf names the user's forwarder rather than the user, so the widget claims and then calls `safeTransferERC20` on the strategy to pass the tokens on, in one batch. +`claim` is **permissionless** — anyone may submit a proof on someone's behalf, and the tokens always go to the recipient named in the leaf. For strategy pools that recipient is the user's forwarder rather than the user, so the tokens reach the user only after `safeTransferERC20` on the strategy. #### How a distribution is built -The tree is assembled off-chain and published to IPFS — the contract stores only the root, the CID and `lastProcessedBlock`. Each leaf is `(recipient, token, cumulativeAmount)`, and a recipient's share of the newly arrived tokens is their stv balance over the effective supply, after the operator's cut: +The tree is assembled off-chain and published to IPFS — the contract stores only the root, the CID and `lastProcessedBlock`. Each leaf is `(recipient, token, cumulativeAmount)`. How a recipient's share is computed depends on the mode the tree was built in: `integral`, the default, weights each holder by how long they held, while `snapshot` uses balances at the chosen block, after the operator's cut: ``` distributable = balance now − (balance at the previous root − claimed since) -share = balanceOf(user) / (totalSupply − balanceOf(pool)) +share = balanceOf(user) / (totalSupply − balanceOf(pool)) // snapshot mode ``` The pool's own stv, minted against the connect deposit, is excluded from the supply. `MANAGER_ROLE` — the Node Operator Manager by default — pushes the root. There is no schedule: a root can be submitted at any time, at most once per block, and has to differ from the current one. :::warning -The share is a **snapshot taken when the tree is built**, not a time-weighted average, and only the current root can be proven against. Two consequences: +Only the current root can be proven against. Two consequences: -- a depositor who exits before claiming loses what they had accrued — the next tree omits their leaf, and the root it replaces is no longer accepted; +- under `snapshot`, a depositor who exits before the tree is built loses what they had accrued — the next tree omits their leaf, and the root it replaces is no longer accepted; - recipients are discovered from `Deposit` events, so an account that received stv by transfer never enters the tree. ::: @@ -366,9 +364,9 @@ The share is a **snapshot taken when the tree is built**, not a time-weighted av Deployment is two transactions, because the pool and the queue reference each other and neither can be constructed first. -**Start** deploys the timelock, both proxies pointed at a dummy implementation, the vault and Dashboard, the queue implementation, the distributor and the pool implementation — then stores a hash of the entire configuration with a **24-hour deadline**. +**Start** deploys the timelock, both proxies pointed at a dummy implementation, the stVault and Dashboard, the queue implementation, the distributor and the pool implementation — then stores a hash of the entire configuration with a **24-hour deadline**. -**Finish** connects the vault to VaultHub (requiring `CONNECT_DEPOSIT` as `msg.value`), upgrades and initializes both proxies, deploys and allowlists the strategy, grants every role, and hands admin to the timelock. +**Finish** connects the stVault to VaultHub (requiring `CONNECT_DEPOSIT` as `msg.value`), upgrades and initializes both proxies, deploys and allowlists the strategy, grants every role, and hands admin to the timelock. The commitment hash binds the caller **and** every configuration field. A different sender, a mutated parameter or a missed deadline all make the finish call revert — a deployment cannot be finished into a different shape than it was started in. @@ -399,7 +397,9 @@ Pausing is per feature, not per contract, so an incident can be contained withou The pause roles go to the emergency committee at deployment; the Dashboard's `PAUSE_BEACON_CHAIN_DEPOSITS_ROLE` goes there too, so the same committee can stop validator deposits. :::warning -**No address holds the resume roles after deployment.** Every implementation constructor pre-pauses its features, and the factory grants only the pause halves. `DEPOSITS_RESUME_ROLE`, `MINTING_RESUME_ROLE`, `WITHDRAWALS_RESUME_ROLE`, `FINALIZE_RESUME_ROLE`, the strategy resume roles and `LOSS_SOCIALIZER_ROLE` are unassigned. +**No address holds the resume roles after deployment.** The factory grants only the pause halves: `DEPOSITS_RESUME_ROLE`, `MINTING_RESUME_ROLE`, `WITHDRAWALS_RESUME_ROLE`, `FINALIZE_RESUME_ROLE`, the strategy resume roles and `LOSS_SOCIALIZER_ROLE` are unassigned. + +A pool goes live unpaused. Pausing is therefore fast and unpausing is not: resuming means granting the resume role and then using it, neither of which any address can do on its own. Both calls fit in one `scheduleBatch` operation, so the cost is a single timelock delay rather than two — plan that delay into any incident response. ::: @@ -467,6 +467,8 @@ sequenceDiagram F ->> E: wstETH ``` +The two halves are independent. ETH and the wstETH amount are separate arguments, each with its own condition, so one entry point covers a fresh deposit, a position built from stv the forwarder already holds, or both at once. + The wstETH lands on the forwarder because minting always credits the caller: `StvStETHPool.mintWsteth` passes `msg.sender` down to `Dashboard.mintWstETH`, and the call was made by the forwarder. The same is true of the debt and of the capacity it is checked against — the position belongs to the forwarder throughout, which is why `remainingMintingCapacitySharesOf(user, ethToFund)` on the strategy resolves it for you. Plain stETH does not appear in this path: `mintWstETH` mints and wraps in one step. An account only holds stETH if it mints through `mintStethShares` directly. @@ -475,7 +477,7 @@ Whatever the shape, the ETH is then staked by the Node Operator through [PDG](.. ### 4.2 Minting stETH -Minting is a separate act from depositing, not a stage of it. It is available in a minting pool to any account holding stv, for any amount within that account's own [capacity](#33-stvstethpool), at any time. +Minting is a separate act from depositing, not a stage of it. It is available in a minting pool to any account holding stv, for any amount within that account's own [capacity](#33-stvstethpool). It requires a fresh oracle report. ```mermaid sequenceDiagram @@ -565,7 +567,7 @@ From here the path is identical to the plain case: the operator finalizes, and t ### 4.4 Rewards -Staking rewards need no distribution transaction. LazyOracle reports the vault's value, `totalAssets()` rises, and every stv holder's claim rises with it. It does require somebody to keep applying reports: a stale report blocks deposits, requests, finalization, minting and forced rebalancing alike. `LazyOracle.updateVaultData` is permissionless, so anyone can do it, but somebody has to. +Staking rewards need no distribution transaction. LazyOracle reports the stVault's value, `totalAssets()` rises, and every stv holder's claim rises with it. It does require somebody to keep applying reports: a stale report blocks deposits, requests, finalization, minting and forced rebalancing alike. `LazyOracle.updateVaultData` is permissionless, so anyone can do it, but somebody has to. ![How a report raises every holder's claim](/img/stvaults/defi-wrapper/staking-rewards-report.png) @@ -575,20 +577,20 @@ $$ \text{assets}(\text{account}) = \text{stv}(\text{account}) \times \frac{\text{totalAssets}}{\text{totalSupply}} $$ -Nothing on the right-hand side changes when a report lands except `totalAssets`, so every claim moves together and in proportion. The diagram above works one through: two depositors fund 10 and 22 ETH, a report lifts the vault to 40 ETH, and their claims become 12.5 and 27.5 ETH. +Nothing on the right-hand side changes when a report lands except `totalAssets`, so every claim moves together and in proportion. The diagram above works one through: two depositors fund 10 and 22 ETH, a report lifts the stVault to 40 ETH, and their claims become 12.5 and 27.5 ETH. It predates the current naming, so it labels the pool "Wrapper" and the token "stvToken", and it shows the report being applied by the Node Operator when in fact `updateVaultData` is permissionless. -Value that arrives as tokens rather than as vault growth — DVT sidecar rewards, points after conversion — is swept out of the vault with `StakingVault.collectERC20` and distributed through the [Distributor](#36-distributor). +Value that arrives as tokens rather than as vault growth — DVT sidecar rewards, points after conversion — is swept out of the stVault with `StakingVault.collectERC20` and distributed through the [Distributor](#36-distributor). ## 5. Risks ### 5.1 From Lido DAO -**Governance capture.** Vaults depend on Lido Core contracts that the DAO can upgrade. That dependency is the surface a compromised or hostile governance would have to work through: in principle an upgrade could replace those contracts with code that moves a vault's ETH. +**Governance capture.** Vaults depend on Lido Core contracts that the DAO can upgrade. That dependency is the surface a compromised or hostile governance would have to work through: in principle an upgrade could replace those contracts with code that moves an stVault's ETH. -Three things stand in the way, which is why this stays theoretical: proposals are watched by the community, Dual Governance lets stakers block one or leave before it takes effect, and a vault can disconnect from Lido Core altogether. +Three things stand in the way, which is why this stays theoretical: proposals are watched by the community, Dual Governance lets stakers block one or leave before it takes effect, and an stVault can disconnect from Lido Core altogether. ### 5.2 From Lido Core @@ -616,6 +618,13 @@ Everything that applies to a plain stVault applies to the pool's vault as well **Strategy economics.** An adapter built on leverage — none of the shipped ones are — carries liquidation risk if the stETH/ETH ratio moves, the risk that pool liquidity is insufficient to close a position, and exposure to rising borrow rates. These come with leverage itself rather than being faults in the design, and a user accepts them when choosing such a strategy. +### 5.6 From the Vault Owner + +**The timelock can move depositor ETH.** `Dashboard.withdraw(recipient, amount)` is guarded by `onlyRoleMemberOrAdmin(WITHDRAW_ROLE)`, which passes for the holder of that role **or** of its admin. +`WITHDRAW_ROLE` has no custom admin, so its admin is `DEFAULT_ADMIN_ROLE`, and the factory grants that to the timelock. A scheduled operation can therefore send up to `withdrawableValue()` to any address: everything not locked as collateral for minted stETH. + +This is the vault ownership model rather than a flaw in the Wrapper — an stVault has an owner, and the Wrapper pools depositors behind that owner. What bounds it is who holds the proposer and executor roles, which is why [Non-custodial operations](../vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/non-custodial-operations.md) matters for anyone choosing a pool. The delay itself is not an exit window: it is an hour in the shipped configurations, while leaving through the withdrawal queue takes days. + ## 6. Useful links - [Architecture overview, detailed information about environments and source code repository](./architecture-overview.md) diff --git a/run-on-lido/stvaults/concepts-and-reference/exit-validators-permissions.md b/run-on-lido/stvaults/concepts-and-reference/exit-validators-permissions.md index 5ed19aa8d..f6fc33831 100644 --- a/run-on-lido/stvaults/concepts-and-reference/exit-validators-permissions.md +++ b/run-on-lido/stvaults/concepts-and-reference/exit-validators-permissions.md @@ -32,7 +32,7 @@ The Vault Owner has two instruments, and only the second one moves ETH: - an empty amounts array, or an amount of **0**, requests a full exit; - a **positive amount** requests a partial withdrawal, and the Consensus Layer keeps at least 32 ETH on the validator. -Partial withdrawals carry three conditions that full exits do not. `VaultHub.triggerValidatorWithdrawals` reverts with `PartialValidatorWithdrawalNotAllowed` unless the report is fresh, the stVault is not in jail, and the stVault has no obligations shortfall. The last condition exists to stop a Vault Owner from filling the withdrawal queue with partial requests to delay the forced exits that would rebalance the stVault. +Partial withdrawals carry three conditions that full exits do not. `VaultHub.triggerValidatorWithdrawals` requires that the report is fresh, the stVault is not in jail, and the stVault has no obligations shortfall. The last condition exists to stop a Vault Owner from filling the withdrawal queue with partial requests to delay the forced exits that would rebalance the stVault. ## stVaults Committee and Lido DAO diff --git a/run-on-lido/stvaults/concepts-and-reference/metrics.md b/run-on-lido/stvaults/concepts-and-reference/metrics.md index b6ce448c6..309f02580 100644 --- a/run-on-lido/stvaults/concepts-and-reference/metrics.md +++ b/run-on-lido/stvaults/concepts-and-reference/metrics.md @@ -380,7 +380,7 @@ $$ \text{carrySpread} = \frac{\text{bottomLine}}{TV_{prev}} \times \frac{\text{secondsInYear}}{\text{periodSeconds}} \times 100\% $$ -Carry Spread is the Health Factor trend indicator: a positive spread raises the Health Factor, a negative one lowers it. +A positive Carry Spread does not guarantee an increase in Health Factor. With a fixed Forced Rebalance Threshold, Health Factor rises when assets grow faster than liabilities in percentage terms and falls when liabilities grow faster than assets. ## Restoring an unhealthy stVault diff --git a/run-on-lido/stvaults/concepts-and-reference/roles-and-permissions.md b/run-on-lido/stvaults/concepts-and-reference/roles-and-permissions.md index 8154525d2..fb1f90a4f 100644 --- a/run-on-lido/stvaults/concepts-and-reference/roles-and-permissions.md +++ b/run-on-lido/stvaults/concepts-and-reference/roles-and-permissions.md @@ -6,7 +6,7 @@ sidebar_label: Roles and Permissions # Roles and Permissions -Every Basic stVault is controlled through a [`Dashboard`](/contracts/dashboard) contract, which is deployed together with the [`StakingVault`](/contracts/staking-vault) and owns it. `Dashboard` uses OpenZeppelin's `AccessControl` with a **two-admin model**: +Every Basic stVault is controlled through a [`Dashboard`](/contracts/dashboard) contract, which is deployed together with the [`StakingVault`](/contracts/staking-vault) and is the owner-side entry point to it. While the stVault is connected, [`VaultHub`](/contracts/vault-hub) holds the `StakingVault` ownership and `Dashboard` acts through it; the ownership comes back to `Dashboard` after a disconnect. `Dashboard` uses OpenZeppelin's `AccessControl` with a **two-admin model**: - **Vault Owner** (`DEFAULT_ADMIN_ROLE`) — the staker side of the stVault. - **Node Operator Manager** (`NODE_OPERATOR_MANAGER_ROLE`) — the validation service side of the stVault. @@ -93,7 +93,7 @@ Granted and revoked by `DEFAULT_ADMIN_ROLE`. | `VAULT_CONFIGURATION_ROLE` | — Request OperatorGrid to change the stVault tier (specify a new tier). | | | — Request OperatorGrid to sync the tier params. | | | — Request OperatorGrid to update the share limit of the stVault. | -| | — Accept a new tier on connection to VaultHub. | +| | — Accept a new tier on connection to VaultHub (`connectAndAcceptTier`) — also needs `DEFAULT_ADMIN_ROLE`. | | `COLLECT_VAULT_ERC20_ROLE` | Collect ERC-20 tokens held by the **stVault** — e.g. recovery of tokens wrongly transferred to the stVault address, or claiming incentives paid to it as ERC-20. Does not support ETH. | ### Node Operator Manager's non-delegable permissions diff --git a/run-on-lido/stvaults/concepts-and-reference/stvaults-technical-design.md b/run-on-lido/stvaults/concepts-and-reference/stvaults-technical-design.md index 722f2ea3a..8d13cd91b 100644 --- a/run-on-lido/stvaults/concepts-and-reference/stvaults-technical-design.md +++ b/run-on-lido/stvaults/concepts-and-reference/stvaults-technical-design.md @@ -145,11 +145,11 @@ To control stETH minting, VaultHub tracks these parameters for each stVault: - **Liability**: The amount of stETH shares minted against the vault (as in, liability towards Lido Core). - **Reserve ratio (RR)**: A portion of the vault's total value locked as an additional reserve (safety buffer) for minted stETH (e.g., if reserve ratio is 30%, with a total value of 100 ETH, 30 ETH must be reserved, which means the vault can mint 70 stETH max). The maximum possible reserve ratio is 99.99%. -- **Force rebalance threshold (FRT)**: When the reserve drops below this threshold, the vault is considered unhealthy and is subject to **force-rebalancing**. FRT must be at least 0.1% lower than RR, e.g. if RR is 30%, FRT must be 29.9% or smaller. +- **Force rebalance threshold (FRT)**: When the reserve drops below this threshold, the vault is considered unhealthy and is subject to **force-rebalancing**. FRT must be more than 0.1% (10 basis points) lower than RR, e.g. if RR is 30%, FRT must be 29.89% or smaller. - **Share limit**: The absolute flat cap on stETH shares that a stVault can mint. - **Obligations**: the health obligation, stETH redemption requests, and Lido fees. -If the vault's locked amount breaches FRT, the vault is considered unhealthy and: +If the vault's reserve falls below FRT, the vault is considered unhealthy and: - cannot mint stETH - cannot withdraw ETH @@ -243,7 +243,7 @@ Any outstanding obligations on a vault: - limit withdrawals from the vault by the amount required to cover obligations; - reduce minting capacity by the amount required to cover obligations; -- pause beacon chain deposits while the vault is unhealthy, has redemptions to cover, or has unsettled fees greater than 1 ETH. This pause prevents the vault from continuously depositing ETH to the consensus layer and avoiding obligation settlement; +- pause beacon chain deposits while the vault is unhealthy, has redemptions to cover, or has unsettled fees of at least 1 ETH. This pause prevents the vault from continuously depositing ETH to the consensus layer and avoiding obligation settlement; - reject attempts to disconnect from VaultHub. _Obligations cheatsheet_ @@ -261,7 +261,7 @@ The reserve ratio ensures that stETH minted by a vault is overcollateralized. Wh Bad debt is resolved through an escalation path: 1. **Vault replenishment**: Voluntary deposit of additional funds to cover the debt -2. **Bad debt socialization**: DAO-initiated shifting of uncovered liability to other vaults operated by the same node operator. The accepting vault must have sufficient capacity to absorb the extra liability without breaching its own health threshold. This keeps the operator responsible for all their vaults rather than isolating losses +2. **Bad debt socialization**: DAO-initiated shifting of uncovered liability to other vaults operated by the same node operator. The amount is capped so that the acceptor's total value still covers its own liability. That can leave the acceptor unhealthy, but never in bad debt itself. This keeps the operator responsible for all their vaults rather than isolating losses 3. **Self-coverage application**: DAO-initiated coverage application mechanisms, e.g. applying a dedicated reserve fund 4. **Bad debt internalization**: As a last resort, Lido DAO is able to write off the vault's remaining bad debt and accept losses to the protocol by decreasing stETH token rebase. @@ -278,7 +278,7 @@ Each vault operation that relies on the accuracy of the vault's total value is g - withdraw ETH from the vault, - mint stETH against the vault, - rebalance the vault, -- deposit to beacon chain, or +- resume beacon-chain deposits once they were paused, or - disconnect from the VaultHub. Staleness therefore seals the vault in a conservative state until a fresh report is submitted, ensuring that collateral calculations never proceed on outdated data. @@ -333,7 +333,7 @@ Diagram. An example group with a 100k limit and three tiers #### Tier change flow -A tier change is performed via a multi-confirmed action (see Dashboard, Multi-confirmation): both the vault owner and the corresponding node operator must independently submit matching tier changes within a set timeframe of each other but regardless of order. Each confirmation is stored on-chain, expires automatically if not completed in time, and can be resubmitted without side effects. Once the second transaction arrives, the contract reallocates the vault’s liability from the old tier to the new one, updates the group and tier share counters, and—if the vault is already connected—pushes the new mint parameters straight to VaultHub. +A tier change is performed via a multi-confirmed action (see Dashboard, Multi-confirmation): both the vault owner and the corresponding node operator must independently submit matching tier changes within a set timeframe of each other but regardless of order. Each confirmation is stored on-chain, expires automatically if not completed in time, and can be resubmitted without side effects. The node operator may pre-confirm a change for a vault that is not connected yet, but the owner's side only goes through for a connected vault. Once the second transaction arrives, the contract reallocates the vault’s liability from the old tier to the new one, updates the group and tier share counters, and pushes the new mint parameters straight to VaultHub. #### Lido fees @@ -346,14 +346,14 @@ A vault can have individual parameters different from its tier: - Share limit: the vault owner and node operator can jointly adjust the vault's share limit independently of the tier's share limit (but not exceeding), - Lido fees: the DAO can update individual vault fees (infrastructure, liquidity, reservation) to differ from tier rates. -These parameters can be restored by mutual confirmation from both the owner and the node operator via the sync tier method. +Syncing a vault back to its tier restores the tier's reserve ratio, forced rebalance threshold and fees, but leaves the vault's own share limit in place. #### Jail OperatorGrid can **jail** a vault as a protective measure. The main purpose of the jailing mechanism is to prevent further minting of a problematic vault. - While jailed, a vault **cannot mint** new stETH shares. -- Jailing **does not** affect burning or other administrative operations; +- Jailing **does not** affect burning or settling obligations; it does block partial validator withdrawals, leaving only full exits; - Jailing can be **set or cleared** by DAO. - Unjailing restores normal minting subject to the usual tier and group limits. @@ -408,7 +408,7 @@ _Diagram. Proven validator deposit flow_ - On connection, VaultHub enforces all predeposits in the vault have sufficient staged balance on the vault. - StakingVaults support [Pectra's EIP-7251](https://eips.ethereum.org/EIPS/eip-7251), so predeposit + activation flows work for both 32 ETH and multi-ETH (up to 2048) validators. - Most steps in PDG can be batched, including a fast path that proves, activates, and tops up multiple validators in a single call. -- A node operator can attach their PDG balance during predeposit. +- A node operator that acts as its own guarantor can top up the PDG guarantee balance with ether attached to the predeposit transaction, in multiples of 1 ETH. - As soon as a validator predeposit is sent, it appears in the beacon deposit queue. Proof of withdrawal credentials can only be generated once the validator is finalized in consensus state. ::: @@ -518,7 +518,7 @@ The fee is disbursed permissionlessly (with an exception for abnormally high fee 3. Updates **settled growth** to the current growth (so the same amount won’t be charged again), 4. Pays the fee to the configured recipient from the vault’s available balance. -On a voluntary disconnect, the fee is disbursed automatically first, then disconnect proceeds. +On a voluntary disconnect the accrued fee is collected, but it is parked on the Dashboard as `feeLeftover` rather than sent: the recipient claims it afterwards with `recoverFeeLeftover()`. ##### Abnormally high fee @@ -536,7 +536,7 @@ Changing the fee rate requires dual confirmation (admin + node-operator manager) - The latest report must be fresh (so accounting is up to date), - Any recent exemptions/corrections to settled growth must have been recorded _before_ that report (prevents retroactive charging), -- The vault must not be under quarantine (ensures that reported total value is not reduced and fully reflects any exemptions). +- Outstanding fees are disbursed at the old rate before the new one takes effect. #### PDG policy @@ -581,7 +581,7 @@ Other scenarios—such as validator consolidation or direct deposits made to the - Can be done in batches. - Uses `0x02` withdrawal credentials pointing to the vault's address. - Does not change `totalValue`. - - Reverts if `locked > totalValue`. + - Reverts if the vault's available balance (its balance less the staged balance) is short of the deposit amount, and is blocked entirely while beacon-chain deposits are paused. 3. **Receiving EL and CL validation rewards** @@ -590,13 +590,13 @@ Other scenarios—such as validator consolidation or direct deposits made to the 4. **Exiting Validators** - - The _vault owner_ can call `requestValidatorExits()` to ask for a voluntary exit. - - The _node operator_, _vault owner_, or `VaultHub` (under extreme conditions) can call `triggerValidatorWithdrawal()` to perform [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002) "triggerable withdrawal". + - The _vault owner_ can call `requestValidatorExit()` to ask for a voluntary exit. + - The _vault owner_ can call `triggerValidatorWithdrawals()` to perform an [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002) "triggerable withdrawal"; the _node operator_ has its own entry point, `ejectValidators()`, which triggers full exits only; and `VaultHub.forceValidatorExit()` does the same under extreme conditions. - Once exited, the validator's balance is transferred to the vault. - - Partial withdrawals may be requested only when the vault is healthy. + - Partial withdrawals are rejected while the vault is jailed or while its obligations exceed its balance; full exits are always available. 5. **Withdrawing** - - The _vault owner_ calls `withdraw()` on VaultHub to take out any amount of **unlocked** ETH (i.e., `totalValue - locked`) from the vault's balance. + - The _vault owner_ calls `withdraw()` on VaultHub to take out **unlocked** ETH (i.e., `totalValue - locked`) from the vault's balance, less anything set aside for redemptions and unsettled Lido fees. - Exiting validators or partial withdrawals are necessary to withdraw staked ETH. #### Accessing stETH @@ -605,7 +605,7 @@ Other scenarios—such as validator consolidation or direct deposits made to the ![Minting flow](/img/stvaults/tech-design/minting-flow.png) -- The _vault owner_ calls `mint()` on the `VaultHub` to mint stETH up to the amount coverable by the locked ether (including RR). +- The _vault owner_ calls `mintShares()` on the `VaultHub` to mint stETH up to the amount coverable by the locked ether (including RR). - Increases the vault's `liabilityShares`. - The minting capacity is limited by current `totalValue`, `liabilityShares`, `shareLimit`, and `reserveRatio`. @@ -617,7 +617,7 @@ Minting against a stVault is subject to the protocol-wide minting [rate limits]( ![Burning flow](/img/stvaults/tech-design/burning-flow.png) -- The _vault owner_ calls `burn()` on the `VaultHub` to burn stETH on behalf of the vault. +- The _vault owner_ calls `burnShares()` on the `VaultHub` to burn stETH on behalf of the vault. - Decreases the vault's `liabilityShares`. - The `locked` amount gets reduced with the next proven update. diff --git a/run-on-lido/stvaults/node-operators/basic-stvaults/consolidation.md b/run-on-lido/stvaults/node-operators/basic-stvaults/consolidation.md index 93af64ab1..2680d2517 100644 --- a/run-on-lido/stvaults/node-operators/basic-stvaults/consolidation.md +++ b/run-on-lido/stvaults/node-operators/basic-stvaults/consolidation.md @@ -124,8 +124,8 @@ Alternatively, you can list pubkeys directly in the command as parameters: ```bash yarn start consolidation write consolidate-validators \ - --source_pubkeys "source_pubkey_first_group_01 source_pubkey_first_group_02, source_pubkey_second_group_01 source_pubkey_second_group_02" \ - --target_pubkeys "target_pubkey_first target_pubkey_second" \ + --source "source_pubkey_first_group_01 source_pubkey_first_group_02, source_pubkey_second_group_01 source_pubkey_second_group_02" \ + --target "target_pubkey_first,target_pubkey_second" \ --wallet-connect ``` diff --git a/run-on-lido/stvaults/node-operators/defi-wrapper/manage-withdrawal-queue.md b/run-on-lido/stvaults/node-operators/defi-wrapper/manage-withdrawal-queue.md index c51f123a6..71dda5b5d 100644 --- a/run-on-lido/stvaults/node-operators/defi-wrapper/manage-withdrawal-queue.md +++ b/run-on-lido/stvaults/node-operators/defi-wrapper/manage-withdrawal-queue.md @@ -77,7 +77,7 @@ yarn start contracts dashboard w trigger-validator-withdrawal The first is a request, not an action: `requestValidatorExit` emits `ValidatorExitRequested` per key and stops there. `eject-validators` is the Node Operator's own instrument — it is checked against the Node Operator address, cannot be delegated, and always performs full exits. Its `` argument is used only to size the fee and to print a confirmation; the contract call carries public keys and a refund recipient, nothing else. -Both on-chain routes pay the EIP-7002 fee per public key. It is set by the network and rises while the withdrawal queue is congested, so read it with `calculateValidatorWithdrawalFee` and send a surplus; the excess is refunded. +Both on-chain routes pay the EIP-7002 fee per public key. It is set by the network and rises while the withdrawal queue is congested. The CLI reads it with `calculateValidatorWithdrawalFee` and sends exactly that; calling the contract directly, send a surplus — the excess is refunded to the recipient. An exit request has to clear the Consensus Layer exit queue and then the sweep before the ETH lands on the stVault. Those two dominate the timeline; the queue's own minimum delay is the smallest part of a depositor's wait. diff --git a/run-on-lido/stvaults/node-operators/defi-wrapper/pdg-shortcut-bootstrap-guide.md b/run-on-lido/stvaults/node-operators/defi-wrapper/pdg-shortcut-bootstrap-guide.md index 6bac2d67b..d228e58eb 100644 --- a/run-on-lido/stvaults/node-operators/defi-wrapper/pdg-shortcut-bootstrap-guide.md +++ b/run-on-lido/stvaults/node-operators/defi-wrapper/pdg-shortcut-bootstrap-guide.md @@ -58,11 +58,13 @@ yarn start contracts dashboard w set-pdg-policy 2 ```bash yarn start vo w role-grant -v \ - -r '[{"account":"","role":"NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE"}]' + -r '[{"account":"","role":"0x5c17b14b08ace6dda14c9642528ae92de2a73d59eacb65c71f39f309a5611063"}]' ``` Signed by the **Node Operator Manager**, which administers this role. +The `role` field is a `bytes32`, not a name: `role-grant` passes it straight to the contract. The hash is `keccak256("vaults.NodeOperatorFee.UnguaranteedDepositRole")`. Omit `-r` to pick the role by name from a prompt instead, or list the vault's roles with `yarn start vo r roles -v `. + ## Step 5. Run the shortcut ```bash @@ -120,7 +122,7 @@ yarn start dw uc tg pool w execute-revoke-role \ ```bash yarn start contracts dashboard w role-revoke \ - '[{"account":"","role":"NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE"}]' + '[{"account":"","role":"0x5c17b14b08ace6dda14c9642528ae92de2a73d59eacb65c71f39f309a5611063"}]' ``` **4. Return the PDG policy to `STRICT`**, so the shortcut cannot be used again without a fresh decision. A timelock proposal, as in Step 3: diff --git a/run-on-lido/stvaults/node-operators/defi-wrapper/update-strategy-implementation.md b/run-on-lido/stvaults/node-operators/defi-wrapper/update-strategy-implementation.md index 35280e456..88424a75a 100644 --- a/run-on-lido/stvaults/node-operators/defi-wrapper/update-strategy-implementation.md +++ b/run-on-lido/stvaults/node-operators/defi-wrapper/update-strategy-implementation.md @@ -69,7 +69,7 @@ Each factory decides what `deployBytes` means. `MellowStrategyFactory` decodes i The strategy proxy's admin is the Timelock Controller, so the upgrade is an ordinary proposal: propose, wait out the delay, execute with the **same salt**. Generate a fresh 32-byte salt for this operation rather than reusing one from an earlier proposal: ```bash -openssl rand -hex 32 +echo 0x$(openssl rand -hex 32) ``` Confirm the admin: diff --git a/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/control-validators.md b/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/control-validators.md index 13fdc76b0..ab38ae81d 100644 --- a/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/control-validators.md +++ b/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/control-validators.md @@ -74,7 +74,7 @@ The fee can spike sharply when the withdrawal queue is congested. Whatever you s ### When partial withdrawals are blocked -Full exits always go through. Partial withdrawals are rejected with `PartialValidatorWithdrawalNotAllowed` when: +Full exits always go through. Partial withdrawals are rejected when: - the stVault has an **obligations shortfall** — anything it owes and cannot currently cover; - the stVault is **jailed**; diff --git a/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/disconnection.md b/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/disconnection.md index 28758229d..3ac347797 100644 --- a/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/disconnection.md +++ b/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/disconnection.md @@ -334,11 +334,13 @@ This is a **permissionless operation** — anyone can apply the report. using Command-line Interface ```bash -yarn start report w submit -v +yarn start report w by-vaults-submit ``` The CLI automatically fetches the latest report CID, retrieves the Merkle tree from IPFS, generates the proof, and submits the transaction. +Use `by-vaults-submit` here rather than `submit`: the `submit` command resolves the stVault's Dashboard first and fails if the owner recorded in VaultHub is an EOA. +
using Etherscan UI diff --git a/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/health-emergency-guide.md b/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/health-emergency-guide.md index 2583e521f..290d4537b 100644 --- a/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/health-emergency-guide.md +++ b/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/health-emergency-guide.md @@ -21,7 +21,7 @@ There are **three ways to restore stVault Health**: ## Definitions -- **Reserve Ratio (RR)** limits minting — this means that users will be able to mint up to 90% of stETH out of ETH they put into the stVault. However, falling below RR doesn't force an immediate correction. +- **Reserve Ratio (RR)** limits minting — this means that users will be able to mint up to (100% − RR) of stETH out of ETH they put into the stVault. However, falling below RR doesn't force an immediate correction. - **Force Rebalance Threshold (FRT)** defines when forced rebalancing is *allowed to* happen. A rebalancing may happen to improve the stVault's health and prevent stVault insolvency. It is always lower than RR: FRT < RR (e.g., 9% if RR = 10%). - **Health Factor (HF)** is a metric calculated using the following formula: Health Factor = Total Value × (1 − FRT) / Minted stETH diff --git a/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/health-monitoring-guide.md b/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/health-monitoring-guide.md index 1ab039b27..2b39966a1 100644 --- a/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/health-monitoring-guide.md +++ b/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/health-monitoring-guide.md @@ -6,7 +6,7 @@ sidebar_position: 3 ## Definitions -- **Reserve Ratio (RR)** limits minting — this means that users will be able to mint up to 90% of stETH out of ETH they put into the stVault. However, falling below RR doesn't force an immediate correction. +- **Reserve Ratio (RR)** limits minting — this means that users will be able to mint up to (100% − RR) of stETH out of ETH they put into the stVault. However, falling below RR doesn't force an immediate correction. - **Utilization Ratio (UR)** is a metric that shows how much stETH is minted out of Total stETH Minting Capacity: Utilization Ratio = (stETH Liability / Total stETH Minting Capacity) × 100% ## TLDR diff --git a/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/rebalance.md b/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/rebalance.md index 4e4d0c4ff..cbf124ce1 100644 --- a/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/rebalance.md +++ b/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/rebalance.md @@ -37,7 +37,7 @@ Rebalancing avoids the market entirely, and it is what the protocol falls back t It depends on the goal: -- **Restore health** — rebalance the shortfall. Read it from `healthShortfallShares` on the `Dashboard` contract: it returns the shares needed to bring the stVault back to a healthy state and cover any pending [Lido redemptions](../../concepts-and-reference/stvaults-technical-design.md#2-redemptions), `0` if the stVault is already healthy, and the maximum `uint256` value if rebalancing alone cannot fix the position. +- **Restore health** — rebalance the shortfall. Read it from `healthShortfallShares` on the `Dashboard` contract: it returns the shares needed to bring the stVault back to a healthy state, `0` if the stVault is already healthy, and the maximum `uint256` value if rebalancing alone cannot fix the position. - **Close the stVault** — rebalance the entire stETH liability, which brings it to zero and unlocks the remaining ETH for withdrawal. See [Disconnection](./disconnection.md). :::note diff --git a/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/validate-setup.md b/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/validate-setup.md index f9e08db26..26e105819 100644 --- a/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/validate-setup.md +++ b/run-on-lido/stvaults/vault-owners-curators-and-stakers/basic-stvaults/validate-setup.md @@ -130,7 +130,7 @@ Open the **Tier** tab in your stVault settings. **Choose Tier** shows which tier using Command-line Interface ```bash -yarn start contracts operator-grid read vault-tier-info +yarn start contracts operator-grid read vault-info ```
diff --git a/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/stakers/supply-withdraw.md b/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/stakers/supply-withdraw.md index 5653cc2cd..50dbe2031 100644 --- a/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/stakers/supply-withdraw.md +++ b/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/stakers/supply-withdraw.md @@ -8,7 +8,7 @@ How to put ETH into a DeFi Wrapper pool and get it back out, through the Web UI ## What a depositor holds -Depositing ETH into a pool mints **stv**, an ERC-20 claim on the pool's share of the underlying stVault. It is transferable, and it grows in value as the stVault earns — nothing is ever distributed to holders, the token just becomes worth more ETH. +Depositing ETH into a pool mints **stv**, an ERC-20 claim on the pool's share of the underlying stVault. It is transferable, and it grows in value as the stVault earns — the yield is never paid out, the token just becomes worth more ETH. $$ \text{assets} = \text{stv}_{\text{account}} \times \frac{\text{totalAssets}}{\text{totalSupply}} @@ -144,8 +144,8 @@ The `owner` you pass at request time is the only address that can later claim. `
using Etherscan UI -1. Open **Etherscan** and navigate to the **pool** contract, then approve the Withdrawal Queue to spend your stv. -2. Navigate to the **WithdrawalQueue** contract and call `requestWithdrawal`, passing the owner, the stv amount and the stETH shares to settle (`0` if none). +1. Open **Etherscan** and navigate to the **WithdrawalQueue** contract — no stv approval is needed. +2. Call `requestWithdrawal`, passing the owner, the stv amount and the stETH shares to settle (`0` if none). 3. Once the request is finalized, call `claimWithdrawal`, passing the recipient and the request id. Check `getWithdrawalStatus` for a request's state, and `getClaimableEther` for what it will pay. diff --git a/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/disconnection.md b/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/disconnection.md index 99c3afaec..78c219109 100644 --- a/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/disconnection.md +++ b/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/disconnection.md @@ -50,23 +50,19 @@ The disconnect process requires multiple roles across the Pool, Withdrawal Queue | `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 | +| `REBALANCE_ROLE` | Dashboard | Rebalance the stVault | | `VOLUNTARY_DISCONNECT_ROLE` | Dashboard | Call `voluntaryDisconnect()` directly (Step 6) | -| `COLLECT_VAULT_ERC20_ROLE` | Dashboard | Transfer wstETH from vault to Distributor via `collectERC20` (Step 7.2) | | `MANAGER_ROLE` | Distributor | `add-token`, `distribute`, and Merkle root updates (Steps 7.3–7.4); pre-granted to `--nodeOperatorManager` unless a different actor distributes | :::info -`VOLUNTARY_DISCONNECT_ROLE` is only needed if the trusted actor calls `voluntaryDisconnect()` directly. If using a Timelock Controller that already holds `DEFAULT_ADMIN_ROLE` on the Dashboard, this grant can be skipped. - -`COLLECT_VAULT_ERC20_ROLE` is only needed if the trusted actor (not the vault owner) performs Step 7.2 (`collect-erc20`). - +Every Dashboard role above is checked with `onlyRoleMemberOrAdmin`, which also passes for holders of the role's admin. None of them has an explicit admin, so the admin is `DEFAULT_ADMIN_ROLE` — the Timelock Controller. A timelock that already holds `DEFAULT_ADMIN_ROLE` on the Dashboard can call `triggerValidatorWithdrawals`, `rebalanceVaultWithShares` and `voluntaryDisconnect` without any of these grants; they exist so a separate trusted actor can act without a delay each time. ::: :::danger `MANAGER_ROLE` on the Distributor is a custody decision rather than an operational one. Its holder sets the Merkle root directly — no delay, and no on-chain check that the tree matches what was actually transferred — so a wrong or malicious root redirects every **unclaimed** token. Amounts users have already claimed are safe, because claims are cumulative per recipient and token, but the remaining balance stays exposed until it is all claimed. ::: -Schedule and execute a batch transaction through the Timelock Controller to grant the roles below. The example covers the seven grants that match the Pool, Withdrawal Queue, and Dashboard **rebalance / pause / exit** path. If the trusted actor must also call `voluntaryDisconnect()` or `collectERC20` on the Dashboard without going through an admin Timelock, append two more `grantRole` calls on the Dashboard for `VOLUNTARY_DISCONNECT_ROLE` and `COLLECT_VAULT_ERC20_ROLE`. `MANAGER_ROLE` is on the Distributor — grant it separately if the distributor is managed by a different address than `--nodeOperatorManager`. +Schedule and execute a batch transaction through the Timelock Controller to grant the roles below. The example covers the seven grants that match the Pool, Withdrawal Queue, and Dashboard **rebalance / pause / exit** path. If the trusted actor must also call `voluntaryDisconnect()` without going through an admin Timelock, append one more `grantRole` call on the Dashboard for `VOLUNTARY_DISCONNECT_ROLE`. `MANAGER_ROLE` is on the Distributor — grant it separately if the distributor is managed by a different address than `--nodeOperatorManager`. ``` targets: [Pool, Pool, Pool, WithdrawalQueue, WithdrawalQueue, Dashboard, Dashboard] @@ -94,8 +90,10 @@ Exit all validators associated with the Staking Vault. This moves ETH from the B yarn start contracts dashboard w trigger-validator-withdrawal ``` +`` and `` are comma-separated lists of equal length. Pass `0` for every amount — a zero amount means a **full** exit, which is what this step needs; a non-zero amount is a partial withdrawal. + :::info -The call must carry the EIP-7002 withdrawal fee, charged per public key. The fee is set by the network and moves from block to block, rising when the withdrawal queue is busy, so read it with `calculateValidatorWithdrawalFee` on the `StakingVault` and send a surplus. The exact amount is taken and the excess is refunded to ``. +The call must carry the EIP-7002 withdrawal fee, charged per public key. The fee is set by the network and moves from block to block, rising when the withdrawal queue is busy. The command reads it with `calculateValidatorWithdrawalFee` on the `StakingVault` and sends exactly that — it adds no surplus, so if the fee rises between the read and inclusion the transaction reverts and you simply resend. The contract takes only what it needs and refunds any excess to ``. ::: Wait for all validator exits to complete and ETH to be swept back to the Staking Vault balance before proceeding. @@ -116,7 +114,7 @@ yarn start dw c wq w pause 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. +`finalize` checks for a fresh oracle report and reverts without one, so apply a report first. Call `WithdrawalQueue.finalize(maxRequests, gasCostCoverageRecipient)` from an account with `FINALIZE_ROLE`: @@ -180,10 +178,10 @@ This burns the account's stv, repays its stETH liability as far as it goes, and To identify undercollateralized accounts, use the health monitoring command: ```bash -yarn start dw uc h r list-unhealthy +yarn start dw uc h r calculate-rebalance-need --verbose ``` -This lists all positions that have breached the forced rebalance threshold. Run `force-rebalance-and-socialize-loss` for each account in the output. +`--verbose` adds a per-account breakdown with an **Undercollateralized** column; run `force-rebalance-and-socialize-loss` only for the accounts marked there. ::: :::warning @@ -191,10 +189,17 @@ This lists all positions that have breached the forced rebalance threshold. Run The pool has a `maxLossSocializationBP` limit that caps how much loss one call may socialize, and it is **`0` by default** — so on a fresh pool this step reverts with `ExcessiveLossSocialization` until the limit is raised. -Raising it is `setMaxLossSocializationBP` on the pool, gated by `DEFAULT_ADMIN_ROLE`, which means a proposal through the Timelock Controller. The CLI has a helper for it, but that helper impersonates the timelock and therefore only works against a forked network — on mainnet or Hoodi, schedule the call the usual way. Do this before Step 5.1, not during it. +Raising it is `setMaxLossSocializationBP` on the pool, gated by `DEFAULT_ADMIN_ROLE`, which means a proposal through the Timelock Controller: + +```bash +yarn start dw uc tg p w propose-set-max-loss-socialization-bp +yarn start dw uc tg p w execute-set-max-loss-socialization-bp +``` + +Do this before Step 5.1, not during it. ::: -### 5.2. Rebalance the vault +### 5.2. Rebalance the stVault Check the current liability: @@ -202,6 +207,8 @@ Check the current liability: yarn start contracts dashboard r liability-shares ``` +That read prints the raw on-chain value; `rebalance-shares` below expects it scaled down by 18 decimals, so divide by `1e18` before passing it on. + Call `Dashboard.rebalanceVaultWithShares(liabilityShares)` from an account with `REBALANCE_ROLE`, passing the full `liabilityShares` amount to bring the liability to zero: ```bash @@ -235,11 +242,13 @@ Either way, keep the window short — Step 7 is what puts the funds back behind ## 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. +After disconnection, remaining ETH in the stVault must be distributed to pool users through the Distributor contract. + +Steps 7.1 and 7.2 write to the `StakingVault`, which is `onlyOwner` once disconnected, so run them from the `newOwner` address chosen in Step 6. No Dashboard role applies here. ### 7.1. Convert vault ETH to an ERC-20 -The Distributor only moves ERC-20 tokens, so the vault's ETH has to become one first. Either wstETH or wETH works, and the choice is economic rather than technical: +The Distributor only moves ERC-20 tokens, so the stVault's ETH has to become one first. Either wstETH or wETH works, and the choice is economic rather than technical: | Token | While the tokens sit unclaimed | Consider it when | | --- | --- | --- | @@ -250,19 +259,19 @@ wstETH is the usual choice for that first reason. Whichever you pick, the rest o The conversion needs no extra step in either case: both contracts mint to the sender on receiving ETH — wstETH stakes it, wETH wraps it — so a single `withdraw` call to the token's address does the job. -First, retrieve the available balance of the vault: +First, retrieve the available balance of the stVault: ```bash yarn start contracts vault r available-balance ``` -Use the value returned as `` in the next command. Call `StakingVault.withdraw(recipient, amount)` with the **token contract address** as the recipient: +That read prints wei; `` below is in ETH, so divide by `1e18` first. Call `StakingVault.withdraw(recipient, amount)` with the **token contract address** as the recipient: ```bash yarn start contracts vault w withdraw ``` -After this call, the vault holds that token rather than ETH. +After this call, the stVault holds that token rather than 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. @@ -270,13 +279,13 @@ Make sure you account for the Initial Connect Deposit (1 ETH) that was unlocked ### 7.2. Transfer wstETH to the Distributor -First, retrieve the wstETH balance of the vault: +First, retrieve the wstETH balance of the stVault: ```bash yarn start account r info ``` -Then send the wstETH from the vault to the Distributor contract using `collectERC20`, passing the retrieved ``: +Then send the wstETH from the stVault to the Distributor contract using `StakingVault.collectERC20`, passing the retrieved ``: ```bash yarn start contracts vault w collect-erc20 @@ -294,7 +303,7 @@ yarn start dw uc distributor w add-token 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. +1. Calculates each user's share — with `--mode=snapshot`, from their stv balance at `--to-block`. 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. @@ -313,6 +322,7 @@ yarn start dw uc distributor w distribute | Option | Description | | --------------------------------------------- | ------------------------------------------------------------------------- | +| `--mode ` | `integral` (default) weights by how long each user held; `snapshot` uses balances at `--to-block` | | `--blacklist ` | Addresses to exclude from distribution | | `--from-block ` / `--to-block ` | Block range for processing transfer events | | `--output-path ` | Path to save the distribution JSON | @@ -360,7 +370,7 @@ Users can claim their funds — see below. After the operator has distributed assets and published the Merkle tree, users can claim their share on the UI. :::info -For `stvStrategyPool` users must first request withdrawal from the underlying DeFi strategy before claiming distributed funds. This pulls funds from the strategy vault back to the proxy balance. The strategy address was shown at pool creation time — if you no longer have it, ask the pool operator. +For `StvStrategyPool` users must first request withdrawal from the underlying DeFi strategy before claiming distributed funds. This pulls funds from the strategy vault back to the proxy balance. The strategy address was shown at pool creation time — if you no longer have it, ask the pool operator. ::: ### Claiming with UI @@ -369,11 +379,11 @@ For `stvStrategyPool` users must first request withdrawal from the underlying De If the UI is unavailable, contact the pool operator for contract addresses and run the UI locally, or use the CLI commands in the next section. ::: -The UI keeps working after the vault is disconnected. Users can still: +The UI keeps working after the stVault is disconnected. Users can still: - 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`, tokens are distributed to proxies but funds can be claimed via UI +- claim any distributed funds. In case of `StvStrategyPool`, tokens are distributed to proxies but funds can be claimed via UI ### Claiming with CLI @@ -393,9 +403,9 @@ You can adjust command with options: - `--tokens [addresses...]` - listing only specific tokens to claim - `--print-only` - only print planned claim -#### stvStrategyPool: claiming distributed funds via CLI +#### StvStrategyPool: claiming distributed funds via CLI -For `stvStrategyPool` the Distributor distributes tokens to each user's **strategy proxy** contract, not directly to the user's wallet. To receive funds, users must first claim to the proxy, then transfer from the proxy to their wallet. +For `StvStrategyPool` the Distributor distributes tokens to each user's **strategy proxy** contract, not directly to the user's wallet. To receive funds, users must first claim to the proxy, then transfer from the proxy to their wallet. **Step 1.** Find your proxy address: diff --git a/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/health-guide.md b/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/health-guide.md index e2d0de6a6..fb2ba3307 100644 --- a/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/health-guide.md +++ b/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/health-guide.md @@ -56,5 +56,5 @@ The amount of ETH required for rebalancing to bring the Utilization Ratio to 100 3. Open the **Contract** tab → **Read as Proxy**. 4. Click **Connect to Web3** and connect your wallet in the dialog window. 5. Find the method `healthShortfallShares` in the list, fill out the input with the `vault` contract address, and click **Query**. - 6. Receive the result right under the submit button. + 6. Receive the result right under the submit button. It is denominated in stETH shares, not ETH.
diff --git a/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/non-custodial-operations.md b/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/non-custodial-operations.md index 523976b04..53c362b78 100644 --- a/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/non-custodial-operations.md +++ b/run-on-lido/stvaults/vault-owners-curators-and-stakers/defi-wrapper/vault-owners-and-curators/non-custodial-operations.md @@ -24,7 +24,7 @@ The setup below splits stVault permissions into two categories, based on whether - A second, broader multisig group is available to act as **Executor** — this should include parties independent from the operations manager, such as a custodian, an auditor, or another trusted counterparty (e.g. a traffic or distribution partner, a builder). :::note -The Proposer/Executor pattern described here is a general non-custodial account design (proposer schedules an action, a separate executor confirms and executes it). It is independent from stVaults' native **Multi-roles confirmation** mechanism, which requires the Vault Owner and Node Operator Manager to jointly confirm a small set of protocol-level parameter changes (NO fee, Confirmation Expiry, AccruedRewardsAdjustment). The two mechanisms can, and should, be used together. +The Proposer/Executor pattern described here is a general non-custodial account design (proposer schedules an action, a separate executor confirms and executes it). It is independent from stVaults' native **Multi-roles confirmation** mechanism, which requires the Vault Owner and Node Operator Manager to jointly confirm a small set of protocol-level parameter changes (Node Operator fee rate, Confirmation Expiry, settled growth correction, stVault ownership transfer). The two mechanisms can, and should, be used together. ::: ## Step 1. Classify roles by custody risk @@ -40,7 +40,7 @@ These roles must never be held directly by a single operational multisig, since | `DEFAULT_ADMIN_ROLE` (Vault Owner) | Can grant or remove any role, including its own, and confirms transfer of StakingVault ownership. | | `WITHDRAW_ROLE` | Withdraws ETH directly from the stVault balance. | | `MINT_ROLE` | Mints stETH from the stVault, creating leveraged exposure on behalf of the owner. | -| `VOLUNTARY_DISCONNECT_ROLE` | Disconnects the stVault from VaultHub — an irreversible structural action, not a routine operational one. | +| `VOLUNTARY_DISCONNECT_ROLE` | Disconnects the stVault from VaultHub — a structural action, not a routine operational one. | | `COLLECT_VAULT_ERC20_ROLE` | Recovers ERC20 tokens sent to the stVault, including incentive tokens that may belong to stakers rather than the operator. | ### Operational roles @@ -52,7 +52,7 @@ These roles support day-to-day management and do not, on their own, allow custod | `FUND_ROLE` | Supplies ETH to the stVault; cannot cause harm. | | `BURN_ROLE` | Repays previously minted stETH; only reduces risk exposure. | | `PAUSE_BEACON_CHAIN_DEPOSITS_ROLE` / `RESUME_BEACON_CHAIN_DEPOSITS_ROLE` | Reversible, does not move capital. | -| `REQUEST_VALIDATOR_EXIT_ROLE` | Initiates a validator exit; returned ETH stays on the stVault balance. | +| `REQUEST_VALIDATOR_EXIT_ROLE` | Requests a validator exit; returned ETH stays on the stVault balance. | | `TRIGGER_VALIDATOR_WITHDRAWAL_ROLE` | Forces a full or partial validator withdrawal; funds return to the stVault balance. | ### Discretionary roles @@ -102,4 +102,4 @@ With this structure: ## Result -With this configuration, the operations manager can run the stVault + DeFi Wrapper day to day — managing the validator lifecycle, adjusting the PDG policy, and funding the stVault with incentives when necessary — without ever holding a role that, on its own, can withdraw funds, mint stETH, or reassign stVault ownership. Every custody-sensitive action requires a second, independent party to execute it. \ No newline at end of file +With this configuration, the operations manager can run the stVault + DeFi Wrapper day to day — managing the validator lifecycle, proposing PDG policy changes through the timelock, and funding the stVault with incentives when necessary — without ever holding a role that, on its own, can withdraw funds, mint stETH, or reassign stVault ownership. Every custody-sensitive action requires a second, independent party to execute it. \ No newline at end of file diff --git a/run-on-lido/stvaults/vault-owners-curators-and-stakers/index.md b/run-on-lido/stvaults/vault-owners-curators-and-stakers/index.md index b77a9496a..b8aff74c0 100644 --- a/run-on-lido/stvaults/vault-owners-curators-and-stakers/index.md +++ b/run-on-lido/stvaults/vault-owners-curators-and-stakers/index.md @@ -8,8 +8,8 @@ sidebar_position: 3 1. [Supply/Withdraw ETH, Mint/Repay stETH](./basic-stvaults/supply-withdraw-mint-repay.md) — the day-to-day operations of any stVault. 2. [Apply Oracle Reports](./basic-stvaults/apply-oracle-reports.md) — applying the latest LazyOracle report before state-dependent operations. -3. [Rebalance](./basic-stvaults/rebalance.md) — simultaneous reducing the stVault's Total Value and stETH Liability together, 1:1. -4. [Control Validators and Withdraw ETH from the Beacon Chain](./basic-stvaults/control-validators.md) — exiting validators, partial withdrawing ETH from validators, and pausing new deposits. +3. [Rebalance](./basic-stvaults/rebalance.md) — simultaneously reducing the stVault's Total Value and stETH Liability together, 1:1. +4. [Control Validators and Withdraw ETH from the Beacon Chain](./basic-stvaults/control-validators.md) — exiting validators, partially withdrawing ETH from validators, and pausing new deposits. 5. [Covering Redemptions with stETH Liquidity](./basic-stvaults/redemptions_coverage_with_steth.md) — minting stETH to pay out redemptions without waiting on the validator exit queue. 6. [Health Monitoring Guide](./basic-stvaults/health-monitoring-guide.md) — Reserve Ratio, Utilization Ratio, and how to track an stVault's health. 7. [Health Emergency Guide](./basic-stvaults/health-emergency-guide.md) — what to do when an stVault approaches or breaches its Force Rebalance Threshold.