Skip to content

Commit 7a98f6d

Browse files
committed
test: improved checks/structure and minor fixes
1 parent 590031b commit 7a98f6d

File tree

1 file changed

+89
-68
lines changed

1 file changed

+89
-68
lines changed

src/test/integration/tests/Rounding.t.sol

Lines changed: 89 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity ^0.8.27;
33

44
import "src/test/integration/IntegrationChecks.t.sol";
55

6+
// TODO: add strategy share manipulation
67
contract Integration_Rounding is IntegrationCheckUtils {
78
using ArrayLib for *;
89
using StdStyle for *;
@@ -16,8 +17,8 @@ contract Integration_Rounding is IntegrationCheckUtils {
1617

1718
OperatorSet mOpSet; // "manipOpSet" used for magnitude manipulation
1819
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
2122

2223
function _init() internal override {
2324
_configAssetTypes(HOLDS_LST);
@@ -26,13 +27,13 @@ contract Integration_Rounding is IntegrationCheckUtils {
2627
badAVS = new AVS("BadAVS"); // AVS is also attacker-controlled
2728
strategy = lstStrats[0];
2829
token = IERC20Metadata(address(strategy.underlyingToken()));
29-
30+
3031
// Prepares to add non-attacker stake into the protocol. Can be any amount > 0.
3132
// Note that the honest stake does not need to be allocated anywhere, so long as it's in the same strategy.
3233
goodStaker = new User("GoodStaker");
3334
deal(address(token), address(goodStaker), uint256(1e18));
3435
goodStaker.depositIntoEigenlayer(strategy.toArray(), 1e18.toArrayU256());
35-
36+
3637
// Register attacker as operator and create attacker-controlled AVS/OpSets
3738
attacker.registerAsOperator(0);
3839
rollForward({blocks: ALLOCATION_CONFIGURATION_DELAY + 1});
@@ -43,117 +44,125 @@ contract Integration_Rounding is IntegrationCheckUtils {
4344
// Register for both opsets
4445
attacker.registerForOperatorSet(mOpSet);
4546
attacker.registerForOperatorSet(rOpSet);
46-
47+
4748
_print("setup");
4849
}
4950

51+
/**
52+
*
53+
* TESTS
54+
*
55+
*/
56+
5057
// TODO: consider incremental manual fuzzing from 1 up to WAD - 1
5158
function test_rounding_allMagsSlashed(
52-
uint64 initialWadToSlash,
59+
uint64 _initialMaxMag,
5360
uint64 _initTokenBalance
5461
) public rand(0) {
5562
vm.pauseGasMetering();
5663
// 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));
5865
// Ensure attacker has at least one token
5966
initTokenBalance = _initTokenBalance > 0 ? _initTokenBalance : _initTokenBalance + 1;
6067
deal(address(token), address(attacker), initTokenBalance);
61-
68+
6269
// Use modifyAllocation+slashOperator to arbitrarily set operator max magnitude
63-
_magnitudeManipulation(initialWadToSlash);
70+
_magnitudeManipulation(_initialMaxMag);
71+
72+
// Deposit all attacker assets into Eigenlayer.
6473
_deposit();
65-
74+
6675
// Perform slashes to gradually whittle down operator magnitude, as well as produce slash escrows.
6776
// Since we're doing multiple slashes, never slash 100%.
6877
for (uint16 i = 0; i < numSlashes; i++) {
6978
uint64 wadToSlash = uint64(cheats.randomUint(1, WAD - 1));
7079
_slash(wadToSlash);
7180
}
72-
81+
7382
// Perform final 100% slash to extract any remaining tokens.
74-
_slash(WAD);
75-
83+
_slash(WAD);
84+
7685
// Release all escrows to the redistributionRecipient (attacker).
7786
_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);
9491
}
95-
92+
9693
function test_rounding_partialMagsSlashed(
97-
uint64 initialWadToSlash,
94+
uint64 _initialMaxMag,
9895
uint64 _initTokenBalance
9996
) public rand(0) {
10097
vm.pauseGasMetering();
10198
// 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));
103100
// Ensure attacker has at least one token
104101
initTokenBalance = _initTokenBalance > 0 ? _initTokenBalance : _initTokenBalance + 1;
105102
deal(address(token), address(attacker), initTokenBalance);
106-
103+
107104
// Use modifyAllocation+slashOperator to arbitrarily set operator max magnitude
108-
_magnitudeManipulation(initialWadToSlash);
105+
_magnitudeManipulation(_initialMaxMag);
106+
107+
// Deposit all attacker assets into Eigenlayer.
109108
_deposit();
110-
109+
111110
// Perform slashes to gradually whittle down operator magnitude, as well as produce slash escrows.
112111
// Since we're doing multiple slashes, never slash 100%.
113112
for (uint16 i = 0; i < numSlashes; i++) {
114113
uint64 wadToSlash = uint64(cheats.randomUint(1, WAD - 1));
115114
_slash(wadToSlash);
116115
}
117-
116+
118117
// Release all escrows to the redistributionRecipient (attacker).
119118
_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 {
133141
if (token.balanceOf(address(attacker)) > initTokenBalance) {
134142
uint64 diff = uint64(token.balanceOf(address(attacker))) - initTokenBalance;
135143
console.log("EXCESS of tokens: %d", diff);
144+
// ANY tokens gained is an exploit.
136145
revert("Rounding error exploit found!");
137146
} else if (token.balanceOf(address(attacker)) < initTokenBalance) {
138147
uint64 diff = uint64(initTokenBalance - token.balanceOf(address(attacker)));
139148
console.log("DEFICIT of tokens: %d", diff);
140-
assertLt(diff, 100, "Tokens lost!");
149+
// Check against provided tolerance.
150+
assertLe(diff, maxLoss, "Tokens lost!");
141151
}
142152
}
143-
153+
144154
// 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 {
146156
// Allocate all magnitude to operator set.
147157
attacker.modifyAllocations(AllocateParams({
148158
operatorSet: mOpSet,
149159
strategies: strategy.toArray(),
150160
newMagnitudes: WAD.toArrayU64()
151161
}));
152-
153-
// TODO: print "newMagnitudes"
154162

155163
_print("allocate");
156164

165+
uint64 wadToSlash = WAD - _initialMaxMag;
157166
// Slash operator to arbitrary mag.
158167
badAVS.slashOperator(SlashingParams({
159168
operator: address(attacker),
@@ -162,20 +171,20 @@ contract Integration_Rounding is IntegrationCheckUtils {
162171
wadsToSlash: uint(wadToSlash).toArrayU256(),
163172
description: "manipulation!"
164173
}));
165-
174+
166175
// TODO: print "wadsToSlash"
167176

168177
_print("slash");
169-
178+
170179
// Deallocate magnitude from operator set.
171180
attacker.modifyAllocations(AllocateParams({
172181
operatorSet: mOpSet,
173182
strategies: strategy.toArray(),
174183
newMagnitudes: 0.toArrayU64()
175184
}));
176-
185+
177186
rollForward({blocks: DEALLOCATION_DELAY + 1});
178-
187+
179188
_print("deallocate");
180189
}
181190

@@ -187,13 +196,13 @@ contract Integration_Rounding is IntegrationCheckUtils {
187196
strategies: strategy.toArray(),
188197
newMagnitudes: (allocatableMagnitude).toArrayU64()
189198
}));
190-
199+
191200
// Deposit all attacker assets into Eigenlayer.
192201
attacker.depositIntoEigenlayer(strategy.toArray(), token.balanceOf(address(attacker)).toArrayU256());
193-
202+
194203
_print("deposit");
195204
}
196-
205+
197206
function _slash(uint64 wadToSlash) internal {
198207
// Perform final slash on redistributable opset and check for profit. Slash all operator magnitude.
199208
badAVS.slashOperator(SlashingParams({
@@ -203,31 +212,43 @@ contract Integration_Rounding is IntegrationCheckUtils {
203212
wadsToSlash: uint(wadToSlash).toArrayU256(),
204213
description: "final slash"
205214
}));
206-
215+
207216
_print("slash");
208217
}
209218

210219
function _release() internal {
211220
// Roll forward past the escrow delay.
212221
rollForward({blocks: slashEscrowFactory.getGlobalEscrowDelay() + 1});
213-
222+
214223
// Release funds.
215224
for (uint32 i = 1; i <= numSlashes; i++) {
216225
vm.prank(address(attacker));
217226
slashEscrowFactory.releaseSlashEscrow(rOpSet, i);
218227
}
219-
228+
220229
// Release final escrow.
221230
vm.prank(address(attacker));
222231
slashEscrowFactory.releaseSlashEscrow(rOpSet, uint256(numSlashes) + 1);
223-
232+
224233
_print("release");
225234
}
226235

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+
227248
function _print(string memory phaseName) internal {
228249
address a = address(attacker);
229250

230-
console.log("");
251+
console.log("");
231252
console.log("===Attacker Info: %s phase===".cyan(), phaseName);
232253

233254
{
@@ -240,7 +261,7 @@ contract Integration_Rounding is IntegrationCheckUtils {
240261
{
241262
console.log("\nShares:".magenta());
242263

243-
(uint[] memory withdrawableArr, uint[] memory depositArr)
264+
(uint[] memory withdrawableArr, uint[] memory depositArr)
244265
= delegationManager.getWithdrawableShares(a, strategy.toArray());
245266
uint withdrawableShares = withdrawableArr.length == 0 ? 0 : withdrawableArr[0];
246267
uint depositShares = depositArr.length == 0 ? 0 : depositArr[0];
@@ -257,7 +278,7 @@ contract Integration_Rounding is IntegrationCheckUtils {
257278

258279
console.log(" - Init Mag: %d", WAD);
259280
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",
261282
allocationManager.getMaxMagnitude(a, strategy),
262283
allocationManager.getEncumberedMagnitude(a, strategy),
263284
allocationManager.getAllocatableMagnitude(a, strategy)

0 commit comments

Comments
 (0)