Skip to content

Commit c9d9cbf

Browse files
committed
Correct handling of dissolve when bond requirement increases
1 parent 85a1c01 commit c9d9cbf

5 files changed

Lines changed: 239 additions & 82 deletions

File tree

contracts/contract/megapool/RocketMegapoolDelegate.sol

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,9 @@ contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDel
376376
validator.lastAssignmentTime = 0;
377377
validators[_validatorId] = validator;
378378
// Decrease total bond used for bond requirement calculations
379-
uint256 recycleValue = uint256(validator.lastRequestedValue) * milliToWei;
380-
(uint256 nodeShare, uint256 userShare) = _calculateCapitalDispersal(recycleValue, getActiveValidatorCount() - 1);
379+
uint256 capitalValue = uint256(validator.lastRequestedValue) * milliToWei;
380+
uint256 recycleValue = capitalValue - prestakeValue;
381+
(uint256 nodeShare, uint256 userShare) = _calculateCapitalDispersal(capitalValue, getActiveValidatorCount() - 1);
381382
nodeBond -= nodeShare;
382383
userCapital -= userShare;
383384
unchecked { // Infeasible overflow
@@ -386,13 +387,22 @@ contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDel
386387
// Snapshot capital ratio
387388
_snapshotCapitalRatio();
388389
// Recycle ETH
389-
assignedValue -= recycleValue - prestakeValue;
390+
assignedValue -= recycleValue;
391+
// Calculate the values to send to node and user
392+
uint256 toUser = userShare;
393+
if (recycleValue < toUser) {
394+
uint256 shortFall = toUser - recycleValue;
395+
toUser -= shortFall;
396+
_increaseDebt(shortFall);
397+
}
398+
uint256 toNode = recycleValue - toUser;
399+
// Send funds
390400
RocketDepositPoolInterface rocketDepositPool = _getRocketDepositPool();
391-
if (userShare > 0) {
392-
rocketDepositPool.recycleDissolvedDeposit{value: userShare}();
401+
if (toUser > 0) {
402+
rocketDepositPool.recycleDissolvedDeposit{value: toUser}();
393403
}
394404
rocketDepositPool.fundsReturned(nodeAddress, nodeShare, userShare);
395-
refundValue += nodeShare - prestakeValue;
405+
refundValue += toNode;
396406
// Emit event
397407
emit MegapoolValidatorDissolved(_validatorId, block.timestamp);
398408
}

test/_helpers/megapool.js

Lines changed: 71 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export async function nodeDeposit(node, bondAmount = '4'.ether, useExpressTicket
6565
rocketDepositPool,
6666
rocketDAOProtocolSettingsDeposit,
6767
rocketMegapoolManager,
68+
linkedListStorage,
6869
] = await Promise.all([
6970
RocketNodeDeposit.deployed(),
7071
RocketNodeManager.deployed(),
@@ -73,6 +74,7 @@ export async function nodeDeposit(node, bondAmount = '4'.ether, useExpressTicket
7374
RocketDepositPool.deployed(),
7475
RocketDAOProtocolSettingsDeposit.deployed(),
7576
RocketMegapoolManager.deployed(),
77+
LinkedListStorage.deployed(),
7678
]);
7779

7880
// Construct deposit data for prestake
@@ -139,15 +141,67 @@ export async function nodeDeposit(node, bondAmount = '4'.ether, useExpressTicket
139141
return data;
140142
}
141143

144+
const megapool = await getMegapoolForNode(node);
145+
142146
const data1 = await getData();
143147

144-
const expressQueueLengthAfterDeposit = useExpressTicket ? data1.expressQueueLength + 1n : data1.expressQueueLength;
145-
const nextAssignmentIsExpress = (queueIndex % (expressQueueRate + 1n) !== expressQueueRate) && expressQueueLengthAfterDeposit !== 0n;
148+
const nextAssignmentIsExpress =
149+
(queueIndex % (expressQueueRate + 1n) !== expressQueueRate) &&
150+
(
151+
(data1.expressQueueLength === 0n && useExpressTicket) ||
152+
(data1.expressQueueLength !== 0n)
153+
);
146154

147155
const assignmentsEnabled = await rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled();
148-
const depositPoolCapacity = await rocketDepositPool.getBalance();
149-
const amountRequired = '32'.ether - bondAmount;
156+
const depositPoolCapacity = await rocketDepositPool.getBalance() + bondAmount;
157+
158+
const expressQueueNamespace = ethers.solidityPackedKeccak256(['string'], ['deposit.queue.express']);
159+
const standardQueueNamespace = ethers.solidityPackedKeccak256(['string'], ['deposit.queue.standard']);
160+
const queueLength = await linkedListStorage.getLength(nextAssignmentIsExpress ? expressQueueNamespace : standardQueueNamespace);
161+
150162
let expectedNodeBalanceChange = bondAmount;
163+
let expectedUserQueuedCapitalChange = '32'.ether - bondAmount;
164+
let expectedNodeQueuedBondChange = bondAmount;
165+
let expectSelfAssignment = false;
166+
let expectImmediateAssignment = false;
167+
let expectStandardQueueChange = useExpressTicket ? 0n : 1n;
168+
let expectExpressQueueChange = useExpressTicket ? 1n : 0n;
169+
let expectExpressTicketsChange = useExpressTicket ? -1n : 0n;
170+
let amountRequired = '32'.ether;
171+
172+
if (assignmentsEnabled) {
173+
if (queueLength > 0n) {
174+
const queueHead = await linkedListStorage.peekItem(nextAssignmentIsExpress ? expressQueueNamespace : standardQueueNamespace);
175+
if (depositPoolCapacity >= amountRequired) {
176+
expectedNodeBalanceChange -= queueHead[2] * milliToWei;
177+
if (nextAssignmentIsExpress) {
178+
expectExpressQueueChange -= 1n;
179+
} else {
180+
expectStandardQueueChange -= 1n;
181+
}
182+
183+
if (queueHead[0] === megapool.target) {
184+
expectedUserQueuedCapitalChange -= '32'.ether - (queueHead[2] * milliToWei);
185+
expectedNodeQueuedBondChange -= queueHead[2] * milliToWei;
186+
expectSelfAssignment = true;
187+
}
188+
}
189+
} else {
190+
if (depositPoolCapacity >= amountRequired) {
191+
expectSelfAssignment = true;
192+
expectImmediateAssignment = true;
193+
expectedUserQueuedCapitalChange = 0n;
194+
expectedNodeQueuedBondChange = 0n;
195+
expectedNodeBalanceChange = 0n;
196+
197+
if (nextAssignmentIsExpress) {
198+
expectExpressQueueChange -= 1n;
199+
} else {
200+
expectStandardQueueChange -= 1n;
201+
}
202+
}
203+
}
204+
}
151205

152206
if (!usingCredit) {
153207
const tx = await rocketNodeDeposit.connect(node).deposit(bondAmount, useExpressTicket, depositData.pubkey, depositData.signature, depositDataRoot, { value: bondAmount });
@@ -157,8 +211,6 @@ export async function nodeDeposit(node, bondAmount = '4'.ether, useExpressTicket
157211
await tx.wait();
158212
}
159213

160-
const megapool = await getMegapoolForNode(node);
161-
162214
const data2 = await getData();
163215

164216
if (!data1.deployed) {
@@ -195,41 +247,12 @@ export async function nodeDeposit(node, bondAmount = '4'.ether, useExpressTicket
195247
assignmentsEnabled &&
196248
depositPoolCapacity >= amountRequired;
197249

198-
let expectSelfAssignment =
199-
expectAssignment &&
200-
(
201-
(useExpressTicket && data1.expressQueueLength === 0n && nextAssignmentIsExpress) ||
202-
(!useExpressTicket && data1.standardQueueLength === 0n)
203-
);
204-
205250
const queueTop = await rocketDepositPool.getQueueTop();
206251
let expectMegapoolAssignment = expectSelfAssignment || (expectAssignment && (queueTop[0] === megapool.target));
207252

208-
if (useExpressTicket) {
209-
assertBN.equal(numExpressTicketsDelta, -1n, 'Did not consume express ticket');
210-
if (expectAssignment) {
211-
if (nextAssignmentIsExpress) {
212-
assertBN.equal(expressQueueLengthDelta, 0n, 'Express queue changed');
213-
assertBN.equal(standardQueueLengthDelta, 0n, 'Standard queue changed');
214-
} else {
215-
assertBN.equal(expressQueueLengthDelta, 1n, 'Express queue did not grow by 1');
216-
assertBN.equal(standardQueueLengthDelta, -1n, 'Standard queue did not shrink by 1');
217-
}
218-
}
219-
} else {
220-
assertBN.equal(numExpressTicketsDelta, 0n, 'Express ticket count incorrect');
221-
if (expectAssignment) {
222-
if (nextAssignmentIsExpress) {
223-
console.log(data1);
224-
console.log(data2);
225-
assertBN.equal(expressQueueLengthDelta, -1n, 'Express queue did not shrink by 1');
226-
assertBN.equal(standardQueueLengthDelta, 1n, 'Standard queue did not grow by 1');
227-
} else {
228-
assertBN.equal(expressQueueLengthDelta, 0n, 'Express queue changed');
229-
assertBN.equal(standardQueueLengthDelta, 0n, 'Standard queue changed');
230-
}
231-
}
232-
}
253+
assertBN.equal(numExpressTicketsDelta, expectExpressTicketsChange, 'Did not consume express ticket');
254+
assertBN.equal(expressQueueLengthDelta, expectExpressQueueChange, 'Express queue length incorrect');
255+
assertBN.equal(standardQueueLengthDelta, expectStandardQueueChange, 'Standard queue length incorrect');
233256

234257
// Confirm state of new validator
235258
const validatorInfo = await getValidatorInfo(megapool, data1.numValidators);
@@ -244,33 +267,24 @@ export async function nodeDeposit(node, bondAmount = '4'.ether, useExpressTicket
244267
assert.equal(validatorInfo.inQueue, true, 'Incorrect validator status');
245268
assert.equal(validatorInfo.inPrestake, false, 'Incorrect validator status');
246269
assertBN.equal(assignedValueDelta, 0n, 'Incorrect assigned value');
247-
assertBN.equal(userQueuedCapitalDelta, launchValue - bondAmount);
248-
assertBN.equal(nodeQueuedBondDelta, bondAmount);
249-
} else if (expectSelfAssignment) {
270+
} else if (expectImmediateAssignment) {
250271
assert.equal(validatorInfo.inQueue, false, 'Incorrect validator status');
251272
assert.equal(validatorInfo.inPrestake, true, 'Incorrect validator status');
252273
assertBN.equal(assignedValueDelta, '31'.ether, 'Incorrect assigned value');
253-
assertBN.equal(nodeBalanceDelta, 0n, 'Incorrect node balance value');
254-
// If validator is assigned immediately, then there should be no change in queued capital balances
255-
assertBN.equal(nodeQueuedBondDelta, 0n);
256-
assertBN.equal(userQueuedCapitalDelta, 0n);
257274
} else {
258275
assert.equal(validatorInfo.inQueue, true, 'Incorrect validator status');
259276
assert.equal(validatorInfo.inPrestake, false, 'Incorrect validator status');
277+
}
260278

261-
if (expectMegapoolAssignment) {
262-
assertBN.equal(assignedValueDelta, '31'.ether, 'Incorrect assigned value');
263-
assertBN.equal(nodeBalanceDelta, 0n, 'Incorrect node balance value');
264-
assertBN.equal(userQueuedCapitalDelta, 0n);
265-
assertBN.equal(nodeQueuedBondDelta, 0n);
266-
} else {
267-
assertBN.equal(assignedValueDelta, 0n, 'Incorrect assigned value');
268-
assertBN.equal(nodeBalanceDelta, expectedNodeBalanceChange, 'Incorrect node balance value');
269-
assertBN.equal(userQueuedCapitalDelta, launchValue - bondAmount);
270-
assertBN.equal(nodeQueuedBondDelta, bondAmount);
271-
}
279+
if (expectSelfAssignment) {
280+
assertBN.equal(assignedValueDelta, '31'.ether, 'Incorrect assigned value');
281+
} else {
282+
assertBN.equal(assignedValueDelta, 0n, 'Incorrect assigned value');
272283
}
273284

285+
assertBN.equal(userQueuedCapitalDelta, expectedUserQueuedCapitalChange, 'Incorrect user queued capital');
286+
assertBN.equal(nodeQueuedBondDelta, expectedNodeQueuedBondChange, 'Incorrect node queued bond');
287+
assertBN.equal(nodeBalanceDelta, expectedNodeBalanceChange, 'Incorrect node balance value');
274288
assertBN.equal(validatorInfo.lastRequestedValue, '32'.ether / milliToWei, 'Incorrect validator lastRequestedValue');
275289
assertBN.equal(validatorInfo.lastRequestedBond, bondAmount / milliToWei, 'Incorrect validator lastRequestedBond');
276290

@@ -497,7 +511,7 @@ export async function getMegapoolForNodeAddress(nodeAddress) {
497511
const megapoolAddress = await rocketMegapoolFactory.getExpectedAddress(nodeAddress);
498512

499513
if (!await rocketMegapoolFactory.getMegapoolDeployed(nodeAddress)) {
500-
return null
514+
return null;
501515
}
502516

503517
const delegateAbi = artifacts.require('RocketMegapoolDelegate').abi;

test/megapool/megapool-tests.js

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,7 @@ export default function() {
927927
it(printTitle('node', 'has bond reduced during dissolve'), async () => {
928928
const dissolvePeriod = (60 * 60 * 24 * 10); // 10 Days
929929
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.time.before.dissolve', dissolvePeriod, { from: owner });
930-
// Deposit 4 ETH
930+
// Deposit 40 ETH
931931
await userDeposit({ from: random, value: '4'.ether * 10n });
932932
// Make 24 validators
933933
for (let i = 0n; i < 24n; i++) {
@@ -964,6 +964,118 @@ export default function() {
964964
assertBN.equal(await megapool.getUserCapital(), '0'.ether);
965965
});
966966

967+
it(printTitle('node', 'can create a new validator if bond requirement increases'), async () => {
968+
const dissolvePeriod = (60 * 60 * 24 * 10); // 10 Days
969+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.time.before.dissolve', dissolvePeriod, { from: owner });
970+
// Reduce reduced.bond to 2 ETH
971+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner });
972+
// Set penalty to 0.1 ETH
973+
const dissolvePenalty = '0.1'.ether
974+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.dissolve.penalty', dissolvePenalty, { from: owner });
975+
// Deposit 56 ETH
976+
await userDeposit({ from: random, value: '20'.ether });
977+
// Make 2 validators with 4 ETH bond
978+
for (let i = 0n; i < 2n; i++) {
979+
await nodeDeposit(node, '4'.ether);
980+
}
981+
// Make 2 validators with 2 ETH bond
982+
for (let i = 0n; i < 2n; i++) {
983+
await nodeDeposit(node, '2'.ether);
984+
}
985+
// Increase reduced.bond back to 4 ETH
986+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '4'.ether, { from: owner });
987+
// Although bond requirement is now 4 ETH, NO is underbonded due to change in bond
988+
await shouldRevert(
989+
nodeDeposit(node, '4'.ether),
990+
'Was able to deposit with 4 ETH',
991+
'Bond requirement not met'
992+
);
993+
// Bond requirement for 5 validators is now 20 ETH and NO has 12, so 8 ETH is required
994+
await nodeDeposit(node, '8'.ether);
995+
});
996+
997+
it(printTitle('node', 'can dissolve and exit validators when underbonded'), async () => {
998+
const dissolvePeriod = (60 * 60 * 24 * 10); // 10 Days
999+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.time.before.dissolve', dissolvePeriod, { from: owner });
1000+
// Reduce reduced.bond to 2 ETH
1001+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner });
1002+
// Set penalty to 0.1 ETH
1003+
const dissolvePenalty = '0.1'.ether
1004+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.dissolve.penalty', dissolvePenalty, { from: owner });
1005+
// Deposit 56 ETH
1006+
await userDeposit({ from: random, value: '20'.ether });
1007+
// Make 2 validators with 4 ETH bond
1008+
for (let i = 0n; i < 2n; i++) {
1009+
await nodeDeposit(node, '4'.ether);
1010+
}
1011+
// Make 52 validators with 2 ETH bond
1012+
for (let i = 0n; i < 52n; i++) {
1013+
await nodeDeposit(node, '2'.ether);
1014+
}
1015+
// Node should now have 4 active (4+4+2+2=12ETH) and 50 queued validators
1016+
assertBN.equal(await megapool.getNodeBond(), '12'.ether);
1017+
assertBN.equal(await megapool.getUserCapital(), '116'.ether);
1018+
assertBN.equal(await megapool.getNodeQueuedBond(), '100'.ether);
1019+
assertBN.equal(await megapool.getUserQueuedCapital(), '1500'.ether);
1020+
// Increase reduced.bond back to 4 ETH
1021+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '4'.ether, { from: owner });
1022+
// Deposit enough to assign 15 validator
1023+
await userDeposit({ from: random, value: '32'.ether * 15n });
1024+
await helpers.time.increase(dissolvePeriod + 1);
1025+
/**
1026+
* The NO now has 19 active validators and 35 queued validators
1027+
*
1028+
* Node bond should be 4 ETH for the first 2, and 2 ETH for the remaining 15 = 42 ETH
1029+
* User capital should be 32 ETH * 19 validators - nodeBond = 566 ETH
1030+
* Node queued bond should be 2 ETH * 35 = 70 ETH
1031+
* User queued capital should be 32 ETH * 35 - nodeQueuedBond = 990 ETH
1032+
*/
1033+
assertBN.equal(await megapool.getNodeBond(), '42'.ether);
1034+
assertBN.equal(await megapool.getUserCapital(), '566'.ether);
1035+
assertBN.equal(await megapool.getNodeQueuedBond(), '70'.ether);
1036+
assertBN.equal(await megapool.getUserQueuedCapital(), '1050'.ether);
1037+
// Disable assignments to prevent exits from performing assignments
1038+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', false, { from: owner });
1039+
// Dissolve 10 validators
1040+
for (let i = 4n; i < 14n; i += 1n) {
1041+
await dissolveValidator(node, i, random);
1042+
}
1043+
/**
1044+
* The NO now has 4 active validators, 10 dissolved validators and 40 queued validators
1045+
*
1046+
* For each of the dissolved validators, the NO was unable to cover the lost 1 ETH so they should have
1047+
* a debt of 10 ETH + 1 ETH in dissolve penalties = 11 ETH
1048+
*
1049+
* Node bond should not have changed = 42 ETH
1050+
* User capital should have decreased by 32 ETH for each dissolved validator = 566 - (32 * 10) = 246 ETH
1051+
*
1052+
* Queued bond/capital should not have changed
1053+
*/
1054+
assertBN.equal(await megapool.getNodeBond(), '42'.ether);
1055+
assertBN.equal(await megapool.getUserCapital(), '246'.ether);
1056+
assertBN.equal(await megapool.getNodeQueuedBond(), '70'.ether);
1057+
assertBN.equal(await megapool.getUserQueuedCapital(), '1050'.ether);
1058+
assertBN.equal(await megapool.getDebt(), (dissolvePenalty + '1'.ether) * 10n);
1059+
// Finish up exiting all remaining validators
1060+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', true, { from: owner });
1061+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.pool.maximum', '10000'.ether, { from: owner });
1062+
// Assign, stake and exit all the queued validators
1063+
for (let i = 14n; i < 54n; i++) {
1064+
await userDeposit({ from: random, value: '32'.ether });
1065+
await helpers.time.increase(dissolvePeriod + 1);
1066+
await stakeMegapoolValidator(megapool, i);
1067+
await exitValidator(megapool, i, '32'.ether);
1068+
}
1069+
// Stake and exit the first 4 validators
1070+
await helpers.time.increase(dissolvePeriod + 1);
1071+
for (let i = 0n; i < 4n; i++) {
1072+
await stakeMegapoolValidator(megapool, i);
1073+
await exitValidator(megapool, i, '32'.ether);
1074+
}
1075+
assertBN.equal(await megapool.getNodeBond(), '0'.ether);
1076+
assertBN.equal(await megapool.getUserCapital(), '0'.ether);
1077+
});
1078+
9671079
it(printTitle('node', 'cannot exit queue to underbonded state due to dissolves'), async () => {
9681080
const dissolvePeriod = (60 * 60 * 24 * 10); // 10 Days
9691081
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMegapool, 'megapool.time.before.dissolve', dissolvePeriod, { from: owner });

0 commit comments

Comments
 (0)