Skip to content

Commit 8cb0428

Browse files
committed
test: This test creates operator shares with no backing.
1 parent bdf57eb commit 8cb0428

File tree

1 file changed

+305
-0
lines changed

1 file changed

+305
-0
lines changed
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
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

Comments
 (0)