Skip to content

Commit b945f1d

Browse files
committed
Fix debt handling on exit balance
1 parent 38cae97 commit b945f1d

File tree

3 files changed

+74
-26
lines changed

3 files changed

+74
-26
lines changed

contracts/contract/megapool/RocketMegapoolDelegate.sol

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -437,26 +437,43 @@ contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDel
437437
/// @dev Internal implementation of claim process
438438
function _claim() internal {
439439
uint256 amountToSend = refundValue;
440+
// If node operator has a debt, pay that off first
440441
if (debt > 0) {
441442
if (debt > amountToSend) {
442-
_reduceDebt(amountToSend);
443-
refundValue = 0;
444-
return;
443+
_repayDebt(amountToSend);
444+
amountToSend = 0;
445445
} else {
446-
_reduceDebt(debt);
447446
amountToSend -= debt;
447+
_repayDebt(debt);
448448
}
449449
}
450-
// Zero out refund
450+
// Zero out refund value
451451
refundValue = 0;
452-
// Send to withdrawal address
453-
address nodeWithdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress);
454-
(bool success,) = nodeWithdrawalAddress.call{value: amountToSend}("");
455-
require(success, "Failed to send ETH");
452+
// If there is still an amount to send after debt, do so now
453+
if (amountToSend > 0) {
454+
address nodeWithdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress);
455+
(bool success,) = nodeWithdrawalAddress.call{value: amountToSend}("");
456+
require(success, "Failed to send ETH");
457+
}
456458
// Emit event
457459
emit RewardsClaimed(amountToSend, block.timestamp);
458460
}
459461

462+
/// @dev Repays a debt from refund balance
463+
function _claimDebt() internal {
464+
// Nothing to do with no debt
465+
if (debt == 0) return;
466+
// Calculate how much to repay
467+
uint256 amountToClaim = refundValue;
468+
if (amountToClaim > debt) {
469+
amountToClaim = debt;
470+
}
471+
// Send to rETH
472+
_repayDebt(amountToClaim);
473+
// Reduce refund balance
474+
refundValue -= amountToClaim;
475+
}
476+
460477
/// @notice Returns the calculated split of pending rewards
461478
function calculatePendingRewards() override external view returns (uint256 nodeRewards, uint256 voterRewards, uint256 rethRewards) {
462479
return calculateRewards(getPendingRewards());
@@ -573,6 +590,7 @@ contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDel
573590
uint256 withdrawableEpoch = uint256(validators[_validatorId].withdrawableEpoch);
574591
uint256 distributableTime = (withdrawableEpoch * secondsPerSlot * slotsPerEpoch + genesisTime) + distributeWindowStart;
575592
require(block.timestamp > distributableTime, "Not enough time has passed");
593+
_claimDebt();
576594
}
577595
}
578596

test/megapool/megapool-tests.js

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
RocketMegapoolFactory,
2323
RocketMegapoolManager,
2424
RocketNodeDeposit,
25-
RocketStorage,
25+
RocketStorage, RocketTokenRETH,
2626
} from '../_utils/artifacts';
2727
import assert from 'assert';
2828
import { stakeMegapoolValidator } from './scenario-stake';
@@ -36,6 +36,7 @@ import { votePenalty } from './scenario-apply-penalty';
3636
import { reduceBond } from './scenario-reduce-bond';
3737
import { dissolveValidator } from './scenario-dissolve';
3838
import { challengeValidator } from './scenario-challenge';
39+
import { repayDebt } from './scenario-repay-debt';
3940

4041
const helpers = require('@nomicfoundation/hardhat-network-helpers');
4142
const hre = require('hardhat');
@@ -984,9 +985,11 @@ export default function() {
984985
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner });
985986
// Notify exit in 5 epochs
986987
const currentEpoch = await getCurrentEpoch();
987-
await notifyExitValidator(megapool, 0, currentEpoch);
988+
await notifyExitValidator(megapool, 0, currentEpoch + 114);
989+
// Increase time to beyond withdrawable_epoch
990+
await helpers.time.increase(12 * 32 * 114);
988991
const nodeBalanceBefore = await ethers.provider.getBalance(nodeWithdrawalAddress);
989-
await notifyFinalBalanceValidator(megapool, 0, '32'.ether, owner, currentEpoch * 32);
992+
await notifyFinalBalanceValidator(megapool, 0, '32'.ether, owner, await getCurrentEpoch() * 32);
990993
const nodeBalanceAfter = await ethers.provider.getBalance(nodeWithdrawalAddress);
991994
/*
992995
NO started with 5 validators
@@ -1002,11 +1005,13 @@ export default function() {
10021005
it(printTitle('node', 'can bond reduce on exit with balance < 32 ETH'), async () => {
10031006
// Adjust `reduced_bond` to 2 ETH
10041007
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner });
1005-
// Notify exit in 5 epochs
1008+
// Notify exit enough into the future to avoid fine
10061009
const currentEpoch = await getCurrentEpoch();
1007-
await notifyExitValidator(megapool, 0, currentEpoch);
1010+
await notifyExitValidator(megapool, 0, currentEpoch + 114);
10081011
const nodeBalanceBefore = await ethers.provider.getBalance(nodeWithdrawalAddress);
1009-
await notifyFinalBalanceValidator(megapool, 0, '32'.ether - '7'.ether, owner, currentEpoch * 32);
1012+
// Increase time to beyond withdrawable_epoch
1013+
await helpers.time.increase(12 * 32 * 114);
1014+
await notifyFinalBalanceValidator(megapool, 0, '32'.ether - '7'.ether, owner, await getCurrentEpoch() * 32);
10101015
const nodeBalanceAfter = await ethers.provider.getBalance(nodeWithdrawalAddress);
10111016
/*
10121017
NO should receive 8 ETH bond on exit, but lost 7 ETH capital so bond should reduce by 8 ETH
@@ -1046,18 +1051,21 @@ export default function() {
10461051
// Notify exit in 112 epochs (1 epoch too late)
10471052
const currentEpoch = await getCurrentEpoch();
10481053
await notifyExitValidator(megapool, 0, currentEpoch + 112);
1049-
// Increase time to beyond withdrawalbe_epoch
1054+
/*
1055+
NO should receive a 0.01 ETH penalty for submitting late
1056+
*/
1057+
const nodeDebt = await megapool.getDebt();
1058+
assertBN.equal(nodeDebt, fineAmount);
1059+
// Increase time to beyond withdrawable_epoch
10501060
await helpers.time.increase(12 * 32 * 112);
10511061
// Increase time to beyond user distribute window
10521062
await helpers.time.increase(userDistributeTime + 1);
10531063
// Submit the final balance from a random account to prevent immediate claim
10541064
const randomMegapoolRunner = megapool.connect(random);
10551065
await notifyFinalBalanceValidator(randomMegapoolRunner, 0, '32'.ether, owner, await getCurrentEpoch() * 32);
1056-
/*
1057-
NO should receive a 0.01 ETH penalty for submitting late
1058-
*/
1059-
const nodeDebt = await megapool.getDebt();
1060-
assertBN.equal(nodeDebt, fineAmount);
1066+
// Debt should be paid on exit
1067+
const nodeDebtAfter = await megapool.getDebt();
1068+
assertBN.equal(nodeDebtAfter, 0n);
10611069
});
10621070

10631071
snapshotDescribe('With debt', () => {
@@ -1074,11 +1082,7 @@ export default function() {
10741082
});
10751083

10761084
it(printTitle('node', 'can manually pay down debt'), async () => {
1077-
const debtBefore = await megapool.getDebt();
1078-
assertBN.isAbove(debtBefore, 0n);
1079-
await megapool.repayDebt({ value: debtBefore });
1080-
const debtAfter = await megapool.getDebt();
1081-
assertBN.equal(debtAfter, '0'.ether);
1085+
await repayDebt(megapool, '1'.ether)
10821086
});
10831087

10841088
it(printTitle('node', 'can use rewards to partially pay down debt'), async () => {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { RocketTokenRETH } from '../_utils/artifacts';
2+
import { assertBN } from '../_helpers/bn';
3+
4+
const hre = require('hardhat');
5+
const ethers = hre.ethers;
6+
7+
export async function repayDebt(megapool, amount) {
8+
const rocketTokenRETH = await RocketTokenRETH.deployed()
9+
async function getData() {
10+
const [ debt, rethBalance ] = await Promise.all([
11+
megapool.getDebt(),
12+
ethers.provider.getBalance(rocketTokenRETH.target),
13+
])
14+
return {debt, rethBalance};
15+
}
16+
17+
const data1 = await getData()
18+
19+
await megapool.repayDebt({ value: amount });
20+
21+
const data2 = await getData()
22+
assertBN.equal(data2.debt, data1.debt - amount);
23+
24+
const balanceDelta = data2.rethBalance - data1.rethBalance;
25+
assertBN.equal(balanceDelta, data1.debt)
26+
}

0 commit comments

Comments
 (0)