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