Skip to content

Commit d44e4b6

Browse files
elhajinnadir-akhtarcursoragent
authored
feat: slash delay and blacklist fix (#1753)
**Motivation:** Burn and redistribution of slashed shares previously settled instantly once an AVS triggered a clear. This left no window to react before funds moved, which is risky for both stakers and AVSs. We want a built-in safety buffer plus a way to halt these flows in an emergency. **Modifications:** - Introduced a 7-day (`50,400` blocks) slash resolution delay before slashed shares can be burned or redistributed. - Added a dedicated pause flag (`PAUSED_BURN_OR_REDISTRIBUTABLE_SHARES`) so burn/redistribution can be paused independently of other flows. - Grandfathered pre-upgrade slashes: existing pending slashes have a zero resolution block and remain immediately clearable after the upgrade. - Added unit, integration, and upgrade tests covering the new behavior. **Result:** - Slashed shares can no longer be burned or redistributed until the 7-day delay has elapsed. - Pauser can pause burn/redistribution specifically without affecting unrelated functionality. --------- Co-authored-by: Nadir Akhtar <9601075+nadir-akhtar@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f84a515 commit d44e4b6

50 files changed

Lines changed: 2478 additions & 607 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/checks.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ jobs:
4747
gethstore.blob.core.windows.net:443
4848
github.com:443
4949
production.cloudflare.docker.com:443
50+
production.cloudfront.docker.com:443
5051
raw.githubusercontent.com:443
5152
registry-1.docker.io:443
5253
release-assets.githubusercontent.com:443

docs/core/DelegationManager.md

Lines changed: 42 additions & 42 deletions
Large diffs are not rendered by default.

docs/core/DurationVaultStrategy.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ struct VaultConfig {
140140

141141
*Requirements*:
142142
* Pause status MUST NOT be set: `PAUSED_NEW_STRATEGIES`
143-
* Token MUST NOT be blacklisted
143+
* Token MUST NOT be EIGEN or bEIGEN
144144
* `vaultAdmin` MUST NOT be zero address
145145
* `arbitrator` MUST NOT be zero address
146146
* `duration` MUST be non-zero and <= `MAX_DURATION` (2 years for now)
@@ -274,7 +274,6 @@ Called by `StrategyManager` on deposit or when re-adding shares when completing
274274

275275
*Requirements* (all must pass):
276276
* State *MUST* be `DEPOSITS` (vault not locked)
277-
* Strategy underlying token *MUST NOT* be blacklisted in `StrategyFactory`
278277
* Staker *MUST* be delegated to the vault
279278
* Deposit amount (in underlying) *MUST NOT* exceed `maxPerDeposit`
280279
* Post-deposit total active shares (in underlying) *MUST NOT* exceed `maxTotalDeposits`
@@ -465,7 +464,6 @@ Rewards follow the standard EigenLayer flow:
465464
| `DurationAlreadyElapsed` | `advanceToWithdrawals()` called at/after `unlockAt` timestamp |
466465
| `VaultNotLocked` | `advanceToWithdrawals()` called before the vault is locked |
467466
| `OperatorIntegrationInvalid` | Invalid operator integration config (zero AVS address) |
468-
| `UnderlyingTokenBlacklisted` | Deposit attempted with blacklisted token |
469467
| `PendingAllocation` | `lock()` attempted with pending allocation modification |
470468
| `MaxPerDepositExceedsMax` | `maxPerDeposit > maxTotalDeposits` or deposit exceeds per-deposit cap |
471469
| `BalanceExceedsMaxTotalDeposits` | Post-deposit balance exceeds TVL cap |

docs/core/EigenPodManager.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,12 @@ The `EigenPodManager` tracks a staker's _deposit shares_ and _beacon chain slash
134134
```solidity
135135
/**
136136
* @notice mapping from pod owner to the deposit shares they have in the virtual beacon chain ETH strategy
137-
*
137+
*
138138
* @dev When an EigenPod registers a balance increase, deposit shares are increased. When registering a balance
139139
* decrease, however, deposit shares are NOT decreased. Instead, the pod owner's beacon chain slashing factor
140140
* is decreased proportional to the balance decrease. This impacts the number of shares that will be withdrawn
141141
* when the deposit shares are queued for withdrawal in the DelegationManager.
142-
*
142+
*
143143
* Note that prior to the slashing release, deposit shares were decreased when balance decreases occurred.
144144
* In certain cases, a combination of queueing a withdrawal plus registering a balance decrease could result
145145
* in a staker having negative deposit shares in this mapping. This negative value would be corrected when the
@@ -320,9 +320,9 @@ function recordBeaconChainETHBalanceUpdate(
320320
nonReentrant
321321
```
322322

323-
This method is called by an `EigenPod` to report a change in its pod owner's shares. It accepts a positive or negative `balanceDeltaWei`. A positive delta is added to the pod owner's _deposit shares,_ and delegated to their operator if applicable. A negative delta is NOT removed from the pod owner's deposit shares. Instead, the proportion of the balance decrease is used to update the pod owner's beacon chain slashing factor and decrease the number of shares delegated to their operator (if applicable). A zero delta results in no change.
323+
This method is called by an `EigenPod` to report a change in its pod owner's shares. It accepts a positive or negative `balanceDeltaWei`. A positive delta is added to the pod owner's _deposit shares,_ and delegated to their operator if applicable. A negative delta is NOT removed from the pod owner's deposit shares. Instead, the proportion of the balance decrease is used to update the pod owner's beacon chain slashing factor and decrease the number of shares delegated to their operator (if applicable). A zero delta results in no change.
324324

325-
**Note** that prior to the slashing release, negative balance deltas subtracted from the pod owner's shares, and could, in certain cases, result in a negative share balance. As of the slashing release, negative balance deltas no longer subtract from share balances, updating the beacon chain slashing factor instead.
325+
**Note** that prior to the slashing release, negative balance deltas subtracted from the pod owner's shares, and could, in certain cases, result in a negative share balance. As of the slashing release, negative balance deltas no longer subtract from share balances, updating the beacon chain slashing factor instead.
326326

327327
If a staker has negative shares as of the slashing release, this method will REVERT, preventing any further balance updates from their pod while the negative share balance persists. In order to fix this and restore the use of their pod, the staker should complete any outstanding withdrawals in the `DelegationManager` "as shares," which will correct the share deficit.
328328

@@ -365,12 +365,12 @@ function increaseBurnOrRedistributableShares(
365365

366366
The `DelegationManager` calls this method when an operator is slashed, calculating the number of slashable shares and marking them for burning here.
367367

368-
Unlike in the `StrategyManager`, there is no current mechanism to burn these shares, as burning requires the Pectra hard fork to be able to eject validators. This will be added in a future update.
368+
Unlike in the `StrategyManager`, there is no current slash-resolution delay, redistribution, or clearing mechanism for these shares. Burning native ETH requires the Pectra hard fork to be able to eject validators.
369369

370-
We also do not distinguish burnable shares on a per operatorSet/slashId basis, like the `StrategyManager` does.
370+
We also do not distinguish burnable ETH shares on a per operatorSet/slashId basis, like the `StrategyManager` does.
371371

372372
*Effects*:
373-
* Increases `burnableShares` for the beacon chain ETH strategy by `addedSharesToBurn`
373+
* Increases `burnableETHShares` for the beacon chain ETH strategy by `addedSharesToBurn`
374374

375375
*Requirements*:
376-
* Can only be called by the `DelegationManager`
376+
* Can only be called by the `DelegationManager`

docs/core/StrategyManager.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -250,11 +250,14 @@ This method directs the `strategy` to convert the input deposit shares to tokens
250250

251251
## Increasing/Clearing Slashed Shares
252252

253-
Slashes shares are marked as burnable or redistributable. Anybody can call
254-
`clearBurnOrRedistributableShares` to send tokens to the slash's `redistributionRecipient`. Shares to clear are stored in `_burnOrRedistributableShares`, a nested [EnumerableMap](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/utils/structs/EnumerableMap.sol). The operatorSet and slashId are used to index into the enumerableMap of strategies to shares. The following methods handle clearing burn or redistributable shares:
253+
Slashed shares are marked as burnable or redistributable. Shares to clear are stored in `_burnOrRedistributableShares`, a nested [EnumerableMap](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/utils/structs/EnumerableMap.sol). The `operatorSet` and `slashId` are used to index into the enumerableMap of strategies to shares.
254+
255+
For slashes recorded after the slash resolution delay was introduced, clearing is delayed until after `SLASH_RESOLUTION_DELAY_BLOCKS` have elapsed. The resolution block for a slash can be queried with `getSlashResolutionBlock(operatorSet, slashId)`, and clearing reverts with `SlashResolutionDelayNotElapsed` until `block.number` is greater than that value. Slashes recorded before this delay was introduced have a resolution block of `0` and remain immediately clearable as grandfathered state.
256+
257+
Anybody can call `clearBurnOrRedistributableShares` or `clearBurnOrRedistributableSharesByStrategy` after the delay to send tokens to the slash's `redistributionRecipient`. Clearing can be paused with `PAUSED_BURNING_AND_REDISTRIBUTION`. The following methods handle clearing burn or redistributable shares:
255258
* [`StrategyManager.increaseBurnOrRedistributableShares`](#increaseburnorredistributableshares)
256-
* [`StrategyManager.clearBurnOrRedistributableShares`](#clearBurnOrRedistributableShares)
257-
* [`StrategyManager.clearBurnOrRedistributableSharesByStrategy](#clearburnorredistributableshares)
259+
* [`StrategyManager.clearBurnOrRedistributableShares`](#clearburnorredistributableshares)
260+
* [`StrategyManager.clearBurnOrRedistributableSharesByStrategy`](#clearburnorredistributableshares)
258261
* [`StrategyManager.burnShares`](#burnshares) - Legacy burnShares function
259262

260263
#### `increaseBurnOrRedistributableShares`
@@ -279,10 +282,11 @@ function increaseBurnOrRedistributableShares(
279282

280283
The `DelegationManager` calls this method when an operator is slashed, calculating the number of slashable shares and marking them for burn or redistribution here.
281284

282-
Anyone can then convert the shares to tokens and trigger a burn via `burnShares`. This asynchronous method was added to mitigate potential DoS vectors when slashing.
285+
The first strategy recorded for an `operatorSet` and `slashId` also sets the slash's resolution block. Additional strategies added for the same slash do not reset the delay. This asynchronous clearing flow was added to mitigate potential DoS vectors when slashing.
283286

284287
*Effects*:
285288
* Sets `burnOrRedistributableShares` for the given `operatorSet`, `slashId`, and `strategy`
289+
* Sets the slash resolution block for the given `operatorSet` and `slashId` if this is the first strategy recorded for that slash
286290
* Emits a `BurnOrRedistributableSharesIncreased` event
287291

288292

@@ -296,22 +300,26 @@ Anyone can then convert the shares to tokens and trigger a burn via `burnShares`
296300

297301
```solidity
298302
/**
299-
* @notice Removes burned shares from storage and transfers the underlying tokens for the slashId to the slash escrow.
303+
* @notice Removes burned shares from storage and transfers the underlying tokens for the slashId to the redistribution recipient.
304+
* @dev Reverts if SLASH_RESOLUTION_DELAY_BLOCKS has not elapsed since the slash was recorded.
305+
* @dev Reverts if clearing is paused via PAUSED_BURNING_AND_REDISTRIBUTION.
300306
* @param operatorSet The operator set to burn shares in.
301307
* @param slashId The slash ID to burn shares in.
302-
* @return The amounts of tokens transferred to the slash escrow for each strategy
308+
* @return The amounts of tokens transferred to the redistribution recipient for each strategy
303309
*/
304310
function clearBurnOrRedistributableShares(
305311
OperatorSet calldata operatorSet,
306312
uint256 slashId)
307313
external returns (uint256[] memory);
308314
309315
/**
310-
* @notice Removes a single strategy's shares from storage and transfers the underlying tokens for the slashId to the slash escrow.
316+
* @notice Removes a single strategy's shares from storage and transfers the underlying tokens for the slashId to the redistribution recipient.
317+
* @dev Reverts if SLASH_RESOLUTION_DELAY_BLOCKS has not elapsed since the slash was recorded.
318+
* @dev Reverts if clearing is paused via PAUSED_BURNING_AND_REDISTRIBUTION.
311319
* @param operatorSet The operator set to burn shares in.
312320
* @param slashId The slash ID to burn shares in.
313321
* @param strategy The strategy to burn shares in.
314-
* @return The amount of shares that were burned.
322+
* @return The amount of tokens transferred to the redistribution recipient.
315323
*/
316324
function clearBurnOrRedistributableSharesByStrategy(
317325
OperatorSet calldata operatorSet,
@@ -320,13 +328,14 @@ function clearBurnOrRedistributableSharesByStrategy(
320328
) external returns (uint256);
321329
```
322330

323-
Anyone can call this method to transfer slashed shares to the operator sets's `redistributionRecipient`. This method sets the `burnOrRedistributableShares` for the given `slashId` and `operatorSet` to 0. To accommodate the unlimited number of strategies that can be added to an operatorSet, users can also pass in a strategy to clear via `clearBurnOrRedistributableSharesByStrategy`. The strategies that haven not been cleared can be retrieved by calling `getBurnOrRedistributableShares(operatorSet, slashId)`.
331+
Anyone can call these methods after the slash resolution delay to transfer slashed shares to the operator set's `redistributionRecipient`. `clearBurnOrRedistributableShares` clears every strategy recorded for the `operatorSet` and `slashId`. To accommodate the unlimited number of strategies that can be added to an operator set, callers can also clear one strategy at a time with `clearBurnOrRedistributableSharesByStrategy`. The strategies that have not been cleared can be retrieved by calling `getBurnOrRedistributableShares(operatorSet, slashId)`.
324332

325333
*Effects*:
326-
* Resets the strategy's burn or redistributable shares for the operatorSet and slashId to 0
334+
* Removes the strategy's burn or redistributable shares for the `operatorSet` and `slashId`
327335
* If the shares to remove are nonzero:
328-
* Calls `withdraw` on the `strategy`, withdrawing shares and sending a corresponding amount of tokens to the operator sets's `redistributionRecipient`
336+
* Calls `withdraw` on the `strategy`, withdrawing shares and sending a corresponding amount of tokens to the operator set's `redistributionRecipient`
329337
* Emits a `BurnOrRedistributableSharesDecreased`
338+
* Deletes the slash resolution block once all strategies for the slash have been cleared
330339

331340
#### `burnShares`
332341

pkg/bindings/DelegationManager/binding.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/bindings/DurationVaultStrategy/binding.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)