Skip to content

Commit b0a2092

Browse files
authored
feat: slashing audit fixes (#1046)
2 parents 7a05fa3 + 44487a0 commit b0a2092

File tree

167 files changed

+11466
-4865
lines changed

Some content is hidden

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

167 files changed

+11466
-4865
lines changed
767 KB
Binary file not shown.
530 KB
Binary file not shown.

docs/core/AllocationManager.md

Lines changed: 122 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ Libraries and Mixins:
2222

2323
## Overview
2424

25-
The `AllocationManager` manages registration and deregistration of operators to operator sets, handles allocation and slashing of operators' slashable stake, and is the entry point an AVS uses to slash an operator. The `AllocationManager's` responsibilities are broken down into the following concepts:
25+
The `AllocationManager` manages AVS metadata registration, registration and deregistration of operators to operator sets, handles allocation and slashing of operators' slashable stake, and is the entry point an AVS uses to slash an operator. The `AllocationManager's` responsibilities are broken down into the following concepts:
26+
27+
* [AVS Metadata](#avs-metadata)
2628
* [Operator Sets](#operator-sets)
2729
* [Allocations and Slashing](#allocations-and-slashing)
2830
* [Config](#config)
@@ -38,6 +40,104 @@ The `AllocationManager` manages registration and deregistration of operators to
3840

3941
---
4042

43+
## AVS Metadata
44+
45+
AVSs must register their metadata to declare themselves who they are as the first step, before they can create operator sets or register operators into operator sets. `AllocationManager` keeps track of AVSs that have registered metadata.
46+
47+
**Methods:**
48+
* [`updateAVSMetadataURI`](#updateavsmetadatauri)
49+
50+
51+
#### `updateAVSMetadataURI`
52+
53+
```solidity
54+
/**
55+
* @notice Called by an AVS to emit an `AVSMetadataURIUpdated` event indicating the information has updated.
56+
*
57+
* @param metadataURI The URI for metadata associated with an AVS.
58+
*
59+
* @dev Note that the `metadataURI` is *never stored* and is only emitted in the `AVSMetadataURIUpdated` event.
60+
*/
61+
function updateAVSMetadataURI(
62+
address avs,
63+
string calldata metadataURI
64+
)
65+
external
66+
checkCanCall(avs)
67+
```
68+
69+
_Note: this method can be called directly by an AVS, or by a caller authorized by the AVS. See [`PermissionController.md`](../permissions/PermissionController.md) for details._
70+
71+
Below is the format AVSs should use when updating their metadata URI initially. This is not validated onchain.
72+
73+
```json
74+
{
75+
"name": "AVS",
76+
"website": "https.avs.xyz/",
77+
"description": "Some description about",
78+
"logo": "http://github.com/logo.png",
79+
"twitter": "https://twitter.com/avs",
80+
}
81+
```
82+
83+
84+
Later on, once AVSs have created operator sets, content in their metadata URI can be updated subsequently.
85+
86+
```json
87+
{
88+
"name": "AVS",
89+
"website": "https.avs.xyz/",
90+
"description": "Some description about",
91+
"logo": "http://github.com/logo.png",
92+
"twitter": "https://twitter.com/avs",
93+
"operatorSets": [
94+
{
95+
"name": "ETH Set",
96+
"id": "1", // Note: we use this param to match the opSet id in the Allocation Manager
97+
"description": "The ETH operatorSet for AVS",
98+
"software": [
99+
{
100+
"name": "NetworkMonitor",
101+
"description": "",
102+
"url": "https://link-to-binary-or-github.com"
103+
},
104+
{
105+
"name": "ValidatorClient",
106+
"description": "",
107+
"url": "https://link-to-binary-or-github.com"
108+
}
109+
],
110+
"slashingConditions": ["Condition A", "Condition B"]
111+
},
112+
{
113+
"name": "EIGEN Set",
114+
"id": "2", // Note: we use this param to match the opSet id in the Allocation Manager
115+
"description": "The EIGEN operatorSet for AVS",
116+
"software": [
117+
{
118+
"name": "NetworkMonitor",
119+
"description": "",
120+
"url": "https://link-to-binary-or-github.com"
121+
},
122+
{
123+
"name": "ValidatorClient",
124+
"description": "",
125+
"url": "https://link-to-binary-or-github.com"
126+
}
127+
],
128+
"slashingConditions": ["Condition A", "Condition B"]
129+
}
130+
]
131+
}
132+
```
133+
134+
*Effects*:
135+
* Emits an `AVSMetadataURIUpdated` event for use in offchain services
136+
137+
*Requirements*:
138+
* Caller MUST be authorized, either as the AVS itself or an admin/appointee (see [`PermissionController.md`](../permissions/PermissionController.md))
139+
140+
41141
## Operator Sets
42142

43143
Operator sets, as described in [ELIP-002](https://github.com/eigenfoundation/ELIPs/blob/main/ELIPs/ELIP-002.md#operator-sets), are useful for AVSs to configure operator groupings which can be assigned different tasks, rewarded based on their strategy allocations, and slashed according to different rules. Operator sets are defined in [`libraries/OperatorSetLib.sol`](../../src/contracts/libraries/OperatorSetLib.sol):
@@ -145,6 +245,7 @@ Optionally, the `avs` can provide a list of `strategies`, specifying which strat
145245

146246
*Requirements*:
147247
* Caller MUST be authorized, either as the AVS itself or an admin/appointee (see [`PermissionController.md`](../permissions/PermissionController.md))
248+
* AVS MUST have registered metadata
148249
* For each `CreateSetParams` element:
149250
* Each `params.operatorSetId` MUST NOT already exist in `_operatorSets[avs]`
150251

@@ -585,7 +686,26 @@ struct SlashingParams {
585686
}
586687
587688
/**
588-
* @notice Called by an AVS to slash an operator in a given operator set
689+
* @notice Called by an AVS to slash an operator in a given operator set. The operator must be registered
690+
* and have slashable stake allocated to the operator set.
691+
*
692+
* @param avs The AVS address initiating the slash.
693+
* @param params The slashing parameters, containing:
694+
* - operator: The operator to slash.
695+
* - operatorSetId: The ID of the operator set the operator is being slashed from.
696+
* - strategies: Array of strategies to slash allocations from (must be in ascending order).
697+
* - wadsToSlash: Array of proportions to slash from each strategy (must be between 0 and 1e18).
698+
* - description: Description of why the operator was slashed.
699+
*
700+
* @dev For each strategy:
701+
* 1. Reduces the operator's current allocation magnitude by wadToSlash proportion.
702+
* 2. Reduces the strategy's max and encumbered magnitudes proportionally.
703+
* 3. If there is a pending deallocation, reduces it proportionally.
704+
* 4. Updates the operator's shares in the DelegationManager.
705+
*
706+
* @dev Small slashing amounts may not result in actual token burns due to
707+
* rounding, which will result in small amounts of tokens locked in the contract
708+
* rather than fully burning through the burn mechanism.
589709
*/
590710
function slashOperator(
591711
address avs,
@@ -646,7 +766,6 @@ Once slashing is processed for a strategy, [slashed stake is burned via the `Del
646766
**Methods:**
647767
* [`setAllocationDelay`](#setallocationdelay)
648768
* [`setAVSRegistrar`](#setavsregistrar)
649-
* [`updateAVSMetadataURI`](#updateavsmetadatauri)
650769

651770
#### `setAllocationDelay`
652771

@@ -737,79 +856,3 @@ Note that when an operator registers, registration will FAIL if the call to `IAV
737856

738857
*Requirements*:
739858
* Caller MUST be authorized, either as the AVS itself or an admin/appointee (see [`PermissionController.md`](../permissions/PermissionController.md))
740-
741-
#### `updateAVSMetadataURI`
742-
743-
```solidity
744-
/**
745-
* @notice Called by an AVS to emit an `AVSMetadataURIUpdated` event indicating the information has updated.
746-
*
747-
* @param metadataURI The URI for metadata associated with an AVS.
748-
*
749-
* @dev Note that the `metadataURI` is *never stored* and is only emitted in the `AVSMetadataURIUpdated` event.
750-
*/
751-
function updateAVSMetadataURI(
752-
address avs,
753-
string calldata metadataURI
754-
)
755-
external
756-
checkCanCall(avs)
757-
```
758-
759-
_Note: this method can be called directly by an AVS, or by a caller authorized by the AVS. See [`PermissionController.md`](../permissions/PermissionController.md) for details._
760-
761-
Below is the format AVSs should use when updating their metadata URI. This is not validated onchain.
762-
763-
```json
764-
{
765-
"name": "AVS",
766-
"website": "https.avs.xyz/",
767-
"description": "Some description about",
768-
"logo": "http://github.com/logo.png",
769-
"twitter": "https://twitter.com/avs",
770-
"operatorSets": [
771-
{
772-
"name": "ETH Set",
773-
"id": "1", // Note: we use this param to match the opSet id in the Allocation Manager
774-
"description": "The ETH operatorSet for AVS",
775-
"software": [
776-
{
777-
"name": "NetworkMonitor",
778-
"description": "",
779-
"url": "https://link-to-binary-or-github.com"
780-
},
781-
{
782-
"name": "ValidatorClient",
783-
"description": "",
784-
"url": "https://link-to-binary-or-github.com"
785-
}
786-
],
787-
"slashingConditions": ["Condition A", "Condition B"]
788-
},
789-
{
790-
"name": "EIGEN Set",
791-
"id": "2", // Note: we use this param to match the opSet id in the Allocation Manager
792-
"description": "The EIGEN operatorSet for AVS",
793-
"software": [
794-
{
795-
"name": "NetworkMonitor",
796-
"description": "",
797-
"url": "https://link-to-binary-or-github.com"
798-
},
799-
{
800-
"name": "ValidatorClient",
801-
"description": "",
802-
"url": "https://link-to-binary-or-github.com"
803-
}
804-
],
805-
"slashingConditions": ["Condition A", "Condition B"]
806-
}
807-
]
808-
}
809-
```
810-
811-
*Effects*:
812-
* Emits an `AVSMetadataURIUpdated` event for use in offchain services
813-
814-
*Requirements*:
815-
* Caller MUST be authorized, either as the AVS itself or an admin/appointee (see [`PermissionController.md`](../permissions/PermissionController.md))

docs/core/EigenPodManager.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,6 @@ Note that the amount of deposit shares removed while in the withdrawal queue may
223223
function addShares(
224224
address staker,
225225
IStrategy strategy,
226-
IERC20,
227226
uint256 shares
228227
)
229228
external

docs/core/StrategyManager.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,11 @@ Note that the amount of deposit shares removed while in the withdrawal queue may
190190
```solidity
191191
/// @notice Used by the DelegationManager to award a Staker some shares that have passed through the withdrawal queue
192192
/// @dev strategy must be beaconChainETH when talking to the EigenPodManager
193-
/// @dev token is not validated; it is only emitted as an event
194193
/// @return existingDepositShares the shares the staker had before any were added
195194
/// @return addedShares the new shares added to the staker's balance
196195
function addShares(
197196
address staker,
198197
IStrategy strategy,
199-
IERC20 token,
200198
uint256 shares
201199
)
202200
external
@@ -252,7 +250,7 @@ This method directs the `strategy` to convert the input deposit shares to tokens
252250

253251
## Burning Slashed Shares
254252

255-
The following methods handle burning of slashed shares:
253+
Slashed shares are marked as burnable, and anyone can call `burnShares` to transfer them to the default burn address. Burnable shares are stored in `burnableShares`, an [EnumerableMap](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/utils/structs/EnumerableMap.sol) with strategy contract addresses as keys and associated view functions. The following methods handle burning of slashed shares:
256254
* [`StrategyManager.increaseBurnableShares`](#increaseburnableshares)
257255
* [`StrategyManager.burnShares`](#burnshares)
258256

@@ -296,11 +294,12 @@ function burnShares(
296294
IStrategy strategy
297295
)
298296
external
299-
onlyDelegationManager
300297
```
301298

302299
Anyone can call this method to burn slashed shares previously added by the `DelegationManager` via `increaseBurnableShares`. This method resets the strategy's burnable shares to 0, and directs the corresponding `strategy` to convert the shares to tokens and transfer them to `DEFAULT_BURN_ADDRESS`, rendering them unrecoverable.
303300

301+
The `strategy` is not called if the strategy had no burnable shares.
302+
304303
*Effects*:
305304
* Resets the strategy's burnable shares to 0
306305
* Calls `withdraw` on the `strategy`, withdrawing shares and sending a corresponding amount of tokens to the `DEFAULT_BURN_ADDRESS`

docs/core/accounting/SharesAccounting.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,67 @@ See implementation in:
162162
* [`StrategyManager.depositIntoStrategy`](../../../src/contracts/core/StrategyManager.sol)
163163
* [`EigenPodManager.recordBeaconChainETHBalanceUpdate`](../../../src/contracts/pods/EigenPodManager.sol)
164164

165+
166+
---
167+
168+
### Delegation
169+
170+
Suppose we have an undelegated staker who decides to delegate to an operator.
171+
We have the following properties that should be preserved.
172+
173+
#### Operator Level
174+
175+
Operator shares should be increased by the amount of delegatable shares the staker has, this is synonymous to their withdrawable shares $a_n$. Therefore,
176+
177+
$$
178+
op_{n+1} = op_{n} + a_n
179+
$$
180+
181+
$$
182+
= op_{n} + s_n k_n l_n m_n
183+
$$
184+
185+
186+
#### Staker Level
187+
188+
withdrawable shares should remain unchanged
189+
190+
$$
191+
a_{n+1} = a_n
192+
$$
193+
194+
deposit shares should remain unchanged
195+
196+
$$
197+
s_{n+1} = s_n
198+
$$
199+
200+
beaconChainSlashingFactor and maxMagnitude should also remain unchanged. In this case, since the staker is not delegated, then their maxMagnitude should by default be equal to 1.
201+
202+
$$
203+
l_{n+1} = l_n
204+
$$
205+
206+
Now the question is what is the new depositScalingFactor equal to?
207+
208+
$$
209+
a_{n+1} = a_n
210+
$$
211+
212+
$$
213+
=> s_{n+1} k_{n+1} l_{n+1} m_{n+1} = s_n k_n l_n m_n
214+
$$
215+
216+
$$
217+
=> s_{n} k_{n+1} l_{n} m_{n+1} = s_n k_n l_n m_n
218+
$$
219+
220+
$$
221+
=> k_{n+1} = \frac {k_n m_n} { m_{n+1} }
222+
$$
223+
224+
Notice how the staker variables that update $k_{n+1}$ and $m_{n+1}$ do not affect previously queued withdrawals and shares received upon withdrawal completion. This is because the maxMagnitude that is looked up is dependent on the operator at the time of the queued withdrawal and the $k_n$ is effectively stored in the scaled shares field.
225+
165226
---
166227

167228
### Slashing
@@ -297,6 +358,8 @@ $$
297358

298359
Note that when a withdrawal is queued, a `Withdrawal` struct is created with _scaled shares_ defined as $q_t = x_t k_t$ where $t$ is the time of the queuing. The reason we define and store scaled shares like this will be clearer in [Complete Withdrawal](#complete-withdrawal) below.
299360

361+
Additionally, we reset the depositScalingFactor when a user queues a withdrawal for all their shares, either through un/redelegation or directly. This is because the DSF at the time of withdrawal is stored in the scaled shares, and any "new" deposits or delegations by the staker should be considered as new. Note that withdrawal completion is treated as a kind of deposit when done as shares, which again will be clearer below.
362+
300363
See implementation in:
301364
* `DelegationManager.queueWithdrawals`
302365
* `SlashingLib.scaleForQueueWithdrawal`

docs/core/accounting/SharesAccountingEdgeCases.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,28 @@ Since the operatorShares are simply incrementing by the exact depositShares, the
145145
Granted the initial deposit amount was `4.418e28` which is magnitudes larger than the discrepancy here but this its important to note the side effects of the redesigned accounting model.
146146
Instead of purely incremented/decremented amounts, we have introduced magnitudes and scaling factor variables which now result in small amounts of rounding error from division in several places. We deem this rounding behavior to be tolerable given the costs associated for the number of transactions to emulate this and the proportional error is very small.
147147

148+
### Slashing and Rounding Up Operator Shares and Rounding down on Staker Withdrawable Shares
149+
150+
As can be observed in the `SlashingLib.sol` library, we round up on the operatorShares when slashing and round down on the staker's withdrawableShares. If we look at a core invariant of the shares accounting model, we ideally want to preserve the following:
151+
152+
$$
153+
op_n = \sum_{i=1}^{k} a_{n,i}
154+
$$
155+
156+
where $op_n$ is the operatorShares at time $n$ and $a_{n,i}$ is the staker's withdrawableShares at time $n$ for the $i^{th}$ staker.
157+
158+
However due to rounding limitations, there will be some error introduced in calculating the amount of operator shares to slash above and also in calculating the staker's withdrawableShares. To prevent a situation where all stakers were to attempt to withdraw and the operatorShares underflows, we round up on the operatorShares when slashing and round down on the staker's withdrawableShares.
159+
160+
So in practice, the above invariant becomes.
161+
162+
$$
163+
op_n \geq \sum_{i=1}^{k} a_{n,i}
164+
$$
165+
166+
Upwards rounding on calculating the amount of operatorShares to give to an operator after slashing is intentionally performed in `SlashingLib.calcSlashedAmount`.
167+
For calculating a staker's withdrawableShares, there are many different factors to consider such as calculating their depositScalingFactor, their slashingFactor, and calculating the amount of withdrawable shares altogether with their depositShares. These variables are all by default rounded down in calculation and is expected behavior for stakers.
168+
169+
148170
## Upper bound on Residual Operator Shares
149171

150172
Related to the above rounding error on deposits, we want to calculate what is the worst case rounding error for a staker depositing shares into EigenLayer.

0 commit comments

Comments
 (0)