Skip to content

Commit 94ad76c

Browse files
feat: release escrow by strategy (#1412)
**Motivation:** We want to ensure: 1. We don't need a max strategy in operatorSet count 2. Blacklisted ERC-20 tokens do not block the escrow release of _other_ tokens **Modifications:** 1. Added a `releaseEscrowByStrategy` functionality, which takes in an strategy & releases that specific strategy 2. Added helper methods that both `releaseEscrow` and `releaseEscrowByStrategy` use 3. Cleaned up some latent DM POC changes **Result:** Handling all edge cases :) --------- Co-authored-by: clandestine.eth <[email protected]>
1 parent d88ee4f commit 94ad76c

File tree

8 files changed

+379
-100
lines changed

8 files changed

+379
-100
lines changed

src/contracts/core/DelegationManager.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ contract DelegationManager is
207207

208208
/// @inheritdoc IDelegationManager
209209
function completeQueuedWithdrawal(
210-
Withdrawal memory withdrawal,
210+
Withdrawal calldata withdrawal,
211211
IERC20[] calldata tokens,
212212
bool receiveAsTokens
213213
) external onlyWhenNotPaused(PAUSED_EXIT_WITHDRAWAL_QUEUE) nonReentrant {

src/contracts/core/SlashEscrowFactory.sol

Lines changed: 96 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -89,25 +89,57 @@ contract SlashEscrowFactory is Initializable, SlashEscrowFactoryStorage, Ownable
8989
) external onlyWhenNotPaused(PAUSED_RELEASE_ESCROW) {
9090
address redistributionRecipient = allocationManager.getRedistributionRecipient(operatorSet);
9191

92-
// If the redistribution recipient is not the default burn address...
93-
if (redistributionRecipient != DEFAULT_BURN_ADDRESS) {
94-
require(msg.sender == redistributionRecipient, OnlyRedistributionRecipient());
95-
}
96-
97-
// Assert that the slash ID is not paused
98-
require(!isEscrowPaused(operatorSet, slashId), IPausable.CurrentlyPaused());
99-
100-
// Assert that the escrow delay has elapsed
101-
require(block.number >= getEscrowCompleteBlock(operatorSet, slashId), EscrowDelayNotElapsed());
92+
_checkReleaseSlashEscrow(operatorSet, slashId, redistributionRecipient);
10293

10394
// Calling `clearBurnOrRedistributableShares` will transfer the underlying tokens to the `SlashEscrow`.
10495
// NOTE: While `clearBurnOrRedistributableShares` may have already been called, we call it again to ensure that the
10596
// underlying tokens are actually in escrow before processing and removing storage (which would otherwise prevent
10697
// the tokens from being released).
10798
strategyManager.clearBurnOrRedistributableShares(operatorSet, slashId);
10899

109-
// Release the slashEscrow. The `SlashEscrow` is deployed in `initiateSlashEscrow`.
110-
_processSlashEscrow(operatorSet, slashId, getSlashEscrow(operatorSet, slashId), redistributionRecipient);
100+
// Process the slash escrow for each strategy.
101+
address[] memory strategies = _pendingStrategiesForSlashId[operatorSet.key()][slashId].values();
102+
for (uint256 i = 0; i < strategies.length; ++i) {
103+
_processSlashEscrowByStrategy({
104+
operatorSet: operatorSet,
105+
slashId: slashId,
106+
slashEscrow: getSlashEscrow(operatorSet, slashId),
107+
redistributionRecipient: redistributionRecipient,
108+
strategy: IStrategy(strategies[i])
109+
});
110+
}
111+
112+
// Update the slash escrow storage.
113+
_updateSlashEscrowStorage(operatorSet, slashId);
114+
}
115+
116+
/// @inheritdoc ISlashEscrowFactory
117+
function releaseSlashEscrowByStrategy(
118+
OperatorSet calldata operatorSet,
119+
uint256 slashId,
120+
IStrategy strategy
121+
) external virtual onlyWhenNotPaused(PAUSED_RELEASE_ESCROW) {
122+
address redistributionRecipient = allocationManager.getRedistributionRecipient(operatorSet);
123+
124+
_checkReleaseSlashEscrow(operatorSet, slashId, redistributionRecipient);
125+
126+
// Calling `clearBurnOrRedistributableSharesByStrategy` will transfer the underlying tokens to the `SlashEscrow`.
127+
// NOTE: While the strategy may have already been cleared, we call it again to ensure that the
128+
// underlying tokens are actually in escrow before processing and removing storage (which would otherwise prevent
129+
// the tokens from being released).
130+
strategyManager.clearBurnOrRedistributableSharesByStrategy(operatorSet, slashId, strategy);
131+
132+
// Release the slashEscrow.
133+
_processSlashEscrowByStrategy({
134+
operatorSet: operatorSet,
135+
slashId: slashId,
136+
slashEscrow: getSlashEscrow(operatorSet, slashId),
137+
redistributionRecipient: redistributionRecipient,
138+
strategy: strategy
139+
});
140+
141+
// Update the slash escrow storage.
142+
_updateSlashEscrowStorage(operatorSet, slashId);
111143
}
112144

113145
/**
@@ -155,48 +187,68 @@ contract SlashEscrowFactory is Initializable, SlashEscrowFactoryStorage, Ownable
155187
*
156188
*/
157189

158-
/// @notice Processes the slash escrow.
159-
function _processSlashEscrow(
190+
/// @notice Checks that the slash escrow can be released.
191+
function _checkReleaseSlashEscrow(
160192
OperatorSet calldata operatorSet,
161193
uint256 slashId,
162-
ISlashEscrow slashEscrow,
163194
address redistributionRecipient
195+
) internal view {
196+
// If the redistribution recipient is not the default burn address...
197+
if (redistributionRecipient != DEFAULT_BURN_ADDRESS) {
198+
require(msg.sender == redistributionRecipient, OnlyRedistributionRecipient());
199+
}
200+
201+
// Assert that the slash ID is not paused
202+
require(!isEscrowPaused(operatorSet, slashId), IPausable.CurrentlyPaused());
203+
204+
// Assert that the escrow delay has elapsed
205+
require(block.number >= getEscrowCompleteBlock(operatorSet, slashId), EscrowDelayNotElapsed());
206+
}
207+
208+
/// @notice Processes the slash escrow for a single strategy.
209+
function _processSlashEscrowByStrategy(
210+
OperatorSet calldata operatorSet,
211+
uint256 slashId,
212+
ISlashEscrow slashEscrow,
213+
address redistributionRecipient,
214+
IStrategy strategy
164215
) internal {
165-
// Create storage pointers for readability.
166-
EnumerableSet.Bytes32Set storage pendingOperatorSets = _pendingOperatorSets;
167-
EnumerableSet.UintSet storage pendingSlashIds = _pendingSlashIds[operatorSet.key()];
216+
// Create storage pointer for readability.
168217
EnumerableSet.AddressSet storage pendingStrategiesForSlashId =
169218
_pendingStrategiesForSlashId[operatorSet.key()][slashId];
170219

171-
// Iterate over the escrow array in reverse order and pop the processed entries from storage.
172-
uint256 totalPendingForSlashId = pendingStrategiesForSlashId.length();
173-
for (uint256 i = totalPendingForSlashId; i > 0; --i) {
174-
address strategy = pendingStrategiesForSlashId.at(i - 1);
175-
176-
// Burn or redistribute the underlying tokens for the strategy.
177-
slashEscrow.releaseTokens(
178-
ISlashEscrowFactory(address(this)),
179-
slashEscrowImplementation,
180-
operatorSet,
181-
slashId,
182-
redistributionRecipient,
183-
IStrategy(strategy)
184-
);
185-
186-
// Remove the strategy and underlying amount from the pending burn or redistributions map.
187-
pendingStrategiesForSlashId.remove(strategy);
188-
emit EscrowComplete(operatorSet, slashId, IStrategy(strategy), redistributionRecipient);
189-
}
220+
// Burn or redistribute the underlying tokens for the strategy.
221+
slashEscrow.releaseTokens({
222+
slashEscrowFactory: ISlashEscrowFactory(address(this)),
223+
slashEscrowImplementation: slashEscrowImplementation,
224+
operatorSet: operatorSet,
225+
slashId: slashId,
226+
recipient: redistributionRecipient,
227+
strategy: strategy
228+
});
229+
230+
// Remove the strategy and underlying amount from the pending strategies escrow map.
231+
pendingStrategiesForSlashId.remove(address(strategy));
232+
emit EscrowComplete(operatorSet, slashId, strategy, redistributionRecipient);
233+
}
234+
235+
function _updateSlashEscrowStorage(OperatorSet calldata operatorSet, uint256 slashId) internal {
236+
// Create storage pointers for readability.
237+
EnumerableSet.Bytes32Set storage pendingOperatorSets = _pendingOperatorSets;
238+
EnumerableSet.UintSet storage pendingSlashIds = _pendingSlashIds[operatorSet.key()];
239+
uint256 totalPendingForSlashId = _pendingStrategiesForSlashId[operatorSet.key()][slashId].length();
190240

191-
// Remove the slash ID from the pending slash IDs set.
192-
pendingSlashIds.remove(slashId);
241+
// If there are no more strategies to process, remove the slash ID from the pending slash IDs set.
242+
if (totalPendingForSlashId == 0) {
243+
pendingSlashIds.remove(slashId);
193244

194-
// Delete the start block for the slash ID.
195-
delete _slashIdToStartBlock[operatorSet.key()][slashId];
245+
// Delete the start block for the slash ID.
246+
delete _slashIdToStartBlock[operatorSet.key()][slashId];
196247

197-
// Remove the operator set from the pending operator sets set if there are no more pending slash IDs.
198-
if (pendingSlashIds.length() == 0) {
199-
pendingOperatorSets.remove(operatorSet.key());
248+
// If there are no more slash IDs for the operator set, remove the operator set from the pending operator sets set.
249+
if (pendingSlashIds.length() == 0) {
250+
pendingOperatorSets.remove(operatorSet.key());
251+
}
200252
}
201253
}
202254

src/contracts/core/SlashEscrowFactoryStorage.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ abstract contract SlashEscrowFactoryStorage is ISlashEscrowFactory {
1616

1717
/// @notice The pause status for the `releaseSlashEscrow` function.
1818
/// @dev Allows all escrow outflows to be temporarily halted.
19-
uint8 public constant PAUSED_RELEASE_ESCROW = 0;
19+
uint8 internal constant PAUSED_RELEASE_ESCROW = 0;
2020

2121
// Immutable Storage
2222

src/contracts/core/StrategyManager.sol

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,11 @@ contract StrategyManager is
174174
OperatorSet calldata operatorSet,
175175
uint256 slashId
176176
) external nonReentrant {
177-
// Iterate over burnable shares backwards. Iterating with an increasing index can cause
178-
// elements to be missed as the item to remove is swapped and popped with the last element.
177+
// Get the strategies to clear.
179178
address[] memory strategies = _burnOrRedistributableShares[operatorSet.key()][slashId].keys();
180179
uint256 length = strategies.length;
181180

181+
// Note: We don't need to iterate backwards since we're indexing into the `EnumerableMap` directly.
182182
for (uint256 i = 0; i < length; ++i) {
183183
clearBurnOrRedistributableSharesByStrategy(operatorSet, slashId, IStrategy(strategies[i]));
184184
}
@@ -194,7 +194,6 @@ contract StrategyManager is
194194
_burnOrRedistributableShares[operatorSet.key()][slashId];
195195

196196
(, uint256 sharesToRemove) = burnOrRedistributableShares.tryGet(address(strategy));
197-
198197
burnOrRedistributableShares.remove(address(strategy));
199198

200199
if (sharesToRemove != 0) {

src/contracts/interfaces/IDelegationManager.sol

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,6 @@ interface IDelegationManagerEvents is IDelegationManagerTypes {
174174

175175
/// @notice Emitted whenever an operator's shares are slashed for a given strategy
176176
event OperatorSharesSlashed(address indexed operator, IStrategy strategy, uint256 totalSlashedShares);
177-
178-
/// @notice Emitted when a redistribution is queued
179-
event RedistributionQueued(bytes32 withdrawalRoot, Withdrawal withdrawal);
180177
}
181178

182179
/**

src/contracts/interfaces/ISlashEscrowFactory.sol

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ interface ISlashEscrowFactory is ISlashEscrowFactoryErrors, ISlashEscrowFactoryE
5757
function initiateSlashEscrow(OperatorSet calldata operatorSet, uint256 slashId, IStrategy strategy) external;
5858

5959
/**
60-
* @notice Releases an escrow.
60+
* @notice Releases an escrow for all strategies in a slash.
6161
* @param operatorSet The operator set whose escrow is being released.
6262
* @param slashId The slash ID of the escrow that is being released.
6363
* @dev The caller must be the redistribution recipient, unless the redistribution recipient
@@ -66,6 +66,21 @@ interface ISlashEscrowFactory is ISlashEscrowFactoryErrors, ISlashEscrowFactoryE
6666
*/
6767
function releaseSlashEscrow(OperatorSet calldata operatorSet, uint256 slashId) external;
6868

69+
/**
70+
* @notice Releases an escrow for a single strategy in a slash.
71+
* @param operatorSet The operator set whose escrow is being released.
72+
* @param slashId The slash ID of the escrow that is being released.
73+
* @param strategy The strategy whose escrow is being released.
74+
* @dev The caller must be the redistribution recipient, unless the redistribution recipient
75+
* is the default burn address in which case anyone can call.
76+
* @dev The slash escrow is released once the delay for ALL strategies has elapsed.
77+
*/
78+
function releaseSlashEscrowByStrategy(
79+
OperatorSet calldata operatorSet,
80+
uint256 slashId,
81+
IStrategy strategy
82+
) external;
83+
6984
/**
7085
* @notice Pauses a escrow.
7186
* @param operatorSet The operator set whose escrow is being paused.

src/test/mocks/StrategyManagerMock.sol

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity ^0.8.27;
44
import "forge-std/Test.sol";
55

66
import "../../contracts/interfaces/IDelegationManager.sol";
7+
import "../../contracts/interfaces/IStrategy.sol";
78

89
contract StrategyManagerMock is Test {
910
IDelegationManager public delegation;
@@ -109,7 +110,12 @@ contract StrategyManagerMock is Test {
109110
return (existingShares, addedShares);
110111
}
111112

112-
function clearBurnOrRedistributableShares(IStrategy strategy, uint sharesToBurn) external {}
113+
function clearBurnOrRedistributableShares(OperatorSet calldata operatorSet, uint slashId) external {}
114+
115+
function clearBurnOrRedistributableSharesByStrategy(OperatorSet calldata operatorSet, uint slashId, IStrategy strategy)
116+
external
117+
returns (uint)
118+
{}
113119

114120
function getBurnOrRedistributableCount(OperatorSet calldata operatorSet, uint slashId) external view returns (uint) {
115121
return _burnOrRedistributableCount;

0 commit comments

Comments
 (0)