@@ -3,6 +3,7 @@ pragma solidity ^0.8.27;
3
3
4
4
import "src/test/integration/IntegrationChecks.t.sol " ;
5
5
6
+ // TODO: add strategy share manipulation
6
7
contract Integration_Rounding is IntegrationCheckUtils {
7
8
using ArrayLib for * ;
8
9
using StdStyle for * ;
@@ -16,8 +17,8 @@ contract Integration_Rounding is IntegrationCheckUtils {
16
17
17
18
OperatorSet mOpSet; // "manipOpSet" used for magnitude manipulation
18
19
OperatorSet rOpSet; // Redistributable opset used to exploit precision loss and trigger redistribution
19
-
20
- uint numSlashes = 50 ;
20
+
21
+ uint16 numSlashes = 100 ; // TODO: transform into envvar
21
22
22
23
function _init () internal override {
23
24
_configAssetTypes (HOLDS_LST);
@@ -26,13 +27,13 @@ contract Integration_Rounding is IntegrationCheckUtils {
26
27
badAVS = new AVS ("BadAVS " ); // AVS is also attacker-controlled
27
28
strategy = lstStrats[0 ];
28
29
token = IERC20Metadata (address (strategy.underlyingToken ()));
29
-
30
+
30
31
// Prepares to add non-attacker stake into the protocol. Can be any amount > 0.
31
32
// Note that the honest stake does not need to be allocated anywhere, so long as it's in the same strategy.
32
33
goodStaker = new User ("GoodStaker " );
33
34
deal (address (token), address (goodStaker), uint256 (1e18 ));
34
35
goodStaker.depositIntoEigenlayer (strategy.toArray (), 1e18 .toArrayU256 ());
35
-
36
+
36
37
// Register attacker as operator and create attacker-controlled AVS/OpSets
37
38
attacker.registerAsOperator (0 );
38
39
rollForward ({blocks: ALLOCATION_CONFIGURATION_DELAY + 1 });
@@ -43,117 +44,125 @@ contract Integration_Rounding is IntegrationCheckUtils {
43
44
// Register for both opsets
44
45
attacker.registerForOperatorSet (mOpSet);
45
46
attacker.registerForOperatorSet (rOpSet);
46
-
47
+
47
48
_print ("setup " );
48
49
}
49
50
51
+ /**
52
+ *
53
+ * TESTS
54
+ *
55
+ */
56
+
50
57
// TODO: consider incremental manual fuzzing from 1 up to WAD - 1
51
58
function test_rounding_allMagsSlashed (
52
- uint64 initialWadToSlash ,
59
+ uint64 _initialMaxMag ,
53
60
uint64 _initTokenBalance
54
61
) public rand (0 ) {
55
62
vm.pauseGasMetering ();
56
63
// Don't slash 100% as we will do multiple slashes
57
- initialWadToSlash = uint64 (bound (initialWadToSlash , 1 , WAD - 1 ));
64
+ _initialMaxMag = uint64 (bound (_initialMaxMag , 1 , WAD - 1 ));
58
65
// Ensure attacker has at least one token
59
66
initTokenBalance = _initTokenBalance > 0 ? _initTokenBalance : _initTokenBalance + 1 ;
60
67
deal (address (token), address (attacker), initTokenBalance);
61
-
68
+
62
69
// Use modifyAllocation+slashOperator to arbitrarily set operator max magnitude
63
- _magnitudeManipulation (initialWadToSlash);
70
+ _magnitudeManipulation (_initialMaxMag);
71
+
72
+ // Deposit all attacker assets into Eigenlayer.
64
73
_deposit ();
65
-
74
+
66
75
// Perform slashes to gradually whittle down operator magnitude, as well as produce slash escrows.
67
76
// Since we're doing multiple slashes, never slash 100%.
68
77
for (uint16 i = 0 ; i < numSlashes; i++ ) {
69
78
uint64 wadToSlash = uint64 (cheats.randomUint (1 , WAD - 1 ));
70
79
_slash (wadToSlash);
71
80
}
72
-
81
+
73
82
// Perform final 100% slash to extract any remaining tokens.
74
- _slash (WAD);
75
-
83
+ _slash (WAD);
84
+
76
85
// Release all escrows to the redistributionRecipient (attacker).
77
86
_release ();
78
-
79
- // Check for any surplus value extracted by attacker. If found, we've proven the existence of the exploit.
80
- // Unchecked to avoid overflow reverting. Safe because token balances are bounded by uint64.
81
- // Negative diff means attacker lost money, positive diff means attacker gained money.
82
- int diff;
83
- unchecked {
84
- diff = int (token.balanceOf (address (attacker)) - initTokenBalance);
85
- }
86
- console.log ("Difference in tokens: %d " , diff);
87
- if (diff > 0 ) {
88
- revert ("Rounding error exploit found! " );
89
- }
90
-
91
- if (diff < 0 ) {
92
- revert ("Tokens lost! " );
93
- }
87
+
88
+ // Check for precision loss.
89
+ // Note: No precision loss expected after all magnitude is slashed.
90
+ checkForPrecisionLoss (0 );
94
91
}
95
-
92
+
96
93
function test_rounding_partialMagsSlashed (
97
- uint64 initialWadToSlash ,
94
+ uint64 _initialMaxMag ,
98
95
uint64 _initTokenBalance
99
96
) public rand (0 ) {
100
97
vm.pauseGasMetering ();
101
98
// Don't slash 100% as we will do multiple slashes
102
- initialWadToSlash = uint64 (bound (initialWadToSlash , 1 , WAD - 1 ));
99
+ _initialMaxMag = uint64 (bound (_initialMaxMag , 1 , WAD - 1 ));
103
100
// Ensure attacker has at least one token
104
101
initTokenBalance = _initTokenBalance > 0 ? _initTokenBalance : _initTokenBalance + 1 ;
105
102
deal (address (token), address (attacker), initTokenBalance);
106
-
103
+
107
104
// Use modifyAllocation+slashOperator to arbitrarily set operator max magnitude
108
- _magnitudeManipulation (initialWadToSlash);
105
+ _magnitudeManipulation (_initialMaxMag);
106
+
107
+ // Deposit all attacker assets into Eigenlayer.
109
108
_deposit ();
110
-
109
+
111
110
// Perform slashes to gradually whittle down operator magnitude, as well as produce slash escrows.
112
111
// Since we're doing multiple slashes, never slash 100%.
113
112
for (uint16 i = 0 ; i < numSlashes; i++ ) {
114
113
uint64 wadToSlash = uint64 (cheats.randomUint (1 , WAD - 1 ));
115
114
_slash (wadToSlash);
116
115
}
117
-
116
+
118
117
// Release all escrows to the redistributionRecipient (attacker).
119
118
_release ();
120
-
121
- // Withdraw all attacker deposits.
122
- (, uint256 [] memory depositShares ) = strategyManager.getDeposits (address (attacker));
123
-
124
- Withdrawal[] memory withdrawals = attacker.queueWithdrawals (strategy.toArray (), depositShares);
125
-
126
- rollForward ({blocks: DELEGATION_MANAGER_MIN_WITHDRAWAL_DELAY_BLOCKS + 1 });
127
-
128
- attacker.completeWithdrawalsAsTokens (withdrawals);
129
-
130
- _print ("withdraw " );
131
-
132
-
119
+
120
+ // Withdraw all attacker deposits. Necessary as operator has partial mags remaining.
121
+ _withdraw ();
122
+
123
+ // Note: Precision loss seems to be a consequence of the DSF, rather than slashing precision loss.
124
+ // Max precision loss for this test is observed to correspond to residual operator shares.
125
+ // TODO: Explore root cause of this precision loss.
126
+ uint operatorShares = delegationManager.getOperatorShares (address (attacker), strategy.toArray ())[0 ];
127
+ checkForPrecisionLoss (operatorShares);
128
+ }
129
+
130
+ /**
131
+ *
132
+ * INTERNAL FUNCTIONS
133
+ *
134
+ */
135
+
136
+ // @notice Check for any precision loss.
137
+ // First case means attacker gained money. If found, we've proven the existence of an exploit.
138
+ // Second case means attacker lost money. This demonstrates precision loss, with maxLoss as the upper bound.
139
+ // @dev Reverts if attacker has gained _any_ tokens, or if token loss is greater than maxLoss.
140
+ function checkForPrecisionLoss (uint256 maxLoss ) internal {
133
141
if (token.balanceOf (address (attacker)) > initTokenBalance) {
134
142
uint64 diff = uint64 (token.balanceOf (address (attacker))) - initTokenBalance;
135
143
console.log ("EXCESS of tokens: %d " , diff);
144
+ // ANY tokens gained is an exploit.
136
145
revert ("Rounding error exploit found! " );
137
146
} else if (token.balanceOf (address (attacker)) < initTokenBalance) {
138
147
uint64 diff = uint64 (initTokenBalance - token.balanceOf (address (attacker)));
139
148
console.log ("DEFICIT of tokens: %d " , diff);
140
- assertLt (diff, 100 , "Tokens lost! " );
149
+ // Check against provided tolerance.
150
+ assertLe (diff, maxLoss, "Tokens lost! " );
141
151
}
142
152
}
143
-
153
+
144
154
// TODO - another way to mess with rounding/precision loss is to manipulate DSF
145
- function _magnitudeManipulation (uint64 wadToSlash ) internal {
155
+ function _magnitudeManipulation (uint64 _initialMaxMag ) internal {
146
156
// Allocate all magnitude to operator set.
147
157
attacker.modifyAllocations (AllocateParams ({
148
158
operatorSet: mOpSet,
149
159
strategies: strategy.toArray (),
150
160
newMagnitudes: WAD.toArrayU64 ()
151
161
}));
152
-
153
- // TODO: print "newMagnitudes"
154
162
155
163
_print ("allocate " );
156
164
165
+ uint64 wadToSlash = WAD - _initialMaxMag;
157
166
// Slash operator to arbitrary mag.
158
167
badAVS.slashOperator (SlashingParams ({
159
168
operator: address (attacker),
@@ -162,20 +171,20 @@ contract Integration_Rounding is IntegrationCheckUtils {
162
171
wadsToSlash: uint (wadToSlash).toArrayU256 (),
163
172
description: "manipulation! "
164
173
}));
165
-
174
+
166
175
// TODO: print "wadsToSlash"
167
176
168
177
_print ("slash " );
169
-
178
+
170
179
// Deallocate magnitude from operator set.
171
180
attacker.modifyAllocations (AllocateParams ({
172
181
operatorSet: mOpSet,
173
182
strategies: strategy.toArray (),
174
183
newMagnitudes: 0 .toArrayU64 ()
175
184
}));
176
-
185
+
177
186
rollForward ({blocks: DEALLOCATION_DELAY + 1 });
178
-
187
+
179
188
_print ("deallocate " );
180
189
}
181
190
@@ -187,13 +196,13 @@ contract Integration_Rounding is IntegrationCheckUtils {
187
196
strategies: strategy.toArray (),
188
197
newMagnitudes: (allocatableMagnitude).toArrayU64 ()
189
198
}));
190
-
199
+
191
200
// Deposit all attacker assets into Eigenlayer.
192
201
attacker.depositIntoEigenlayer (strategy.toArray (), token.balanceOf (address (attacker)).toArrayU256 ());
193
-
202
+
194
203
_print ("deposit " );
195
204
}
196
-
205
+
197
206
function _slash (uint64 wadToSlash ) internal {
198
207
// Perform final slash on redistributable opset and check for profit. Slash all operator magnitude.
199
208
badAVS.slashOperator (SlashingParams ({
@@ -203,31 +212,43 @@ contract Integration_Rounding is IntegrationCheckUtils {
203
212
wadsToSlash: uint (wadToSlash).toArrayU256 (),
204
213
description: "final slash "
205
214
}));
206
-
215
+
207
216
_print ("slash " );
208
217
}
209
218
210
219
function _release () internal {
211
220
// Roll forward past the escrow delay.
212
221
rollForward ({blocks: slashEscrowFactory.getGlobalEscrowDelay () + 1 });
213
-
222
+
214
223
// Release funds.
215
224
for (uint32 i = 1 ; i <= numSlashes; i++ ) {
216
225
vm.prank (address (attacker));
217
226
slashEscrowFactory.releaseSlashEscrow (rOpSet, i);
218
227
}
219
-
228
+
220
229
// Release final escrow.
221
230
vm.prank (address (attacker));
222
231
slashEscrowFactory.releaseSlashEscrow (rOpSet, uint256 (numSlashes) + 1 );
223
-
232
+
224
233
_print ("release " );
225
234
}
226
235
236
+ function _withdraw () internal {
237
+ (, uint256 [] memory depositShares ) = strategyManager.getDeposits (address (attacker));
238
+
239
+ Withdrawal[] memory withdrawals = attacker.queueWithdrawals (strategy.toArray (), depositShares);
240
+
241
+ rollForward ({blocks: DELEGATION_MANAGER_MIN_WITHDRAWAL_DELAY_BLOCKS + 1 });
242
+
243
+ attacker.completeWithdrawalsAsTokens (withdrawals);
244
+
245
+ _print ("withdraw " );
246
+ }
247
+
227
248
function _print (string memory phaseName ) internal {
228
249
address a = address (attacker);
229
250
230
- console.log ("" );
251
+ console.log ("" );
231
252
console.log ("===Attacker Info: %s phase=== " .cyan (), phaseName);
232
253
233
254
{
@@ -240,7 +261,7 @@ contract Integration_Rounding is IntegrationCheckUtils {
240
261
{
241
262
console.log ("\nShares: " .magenta ());
242
263
243
- (uint [] memory withdrawableArr , uint [] memory depositArr )
264
+ (uint [] memory withdrawableArr , uint [] memory depositArr )
244
265
= delegationManager.getWithdrawableShares (a, strategy.toArray ());
245
266
uint withdrawableShares = withdrawableArr.length == 0 ? 0 : withdrawableArr[0 ];
246
267
uint depositShares = depositArr.length == 0 ? 0 : depositArr[0 ];
@@ -257,7 +278,7 @@ contract Integration_Rounding is IntegrationCheckUtils {
257
278
258
279
console.log (" - Init Mag: %d " , WAD);
259
280
console.log (
260
- " - Max Mag: %d\n -- Total Allocated: %d\n -- Total Available: %d " ,
281
+ " - Max Mag: %d\n -- Total Allocated: %d\n -- Total Available: %d " ,
261
282
allocationManager.getMaxMagnitude (a, strategy),
262
283
allocationManager.getEncumberedMagnitude (a, strategy),
263
284
allocationManager.getAllocatableMagnitude (a, strategy)
0 commit comments