Skip to content

Commit 6c6d52d

Browse files
committed
Work on penalties and bond reduction
1 parent 027a63b commit 6c6d52d

File tree

7 files changed

+121
-38
lines changed

7 files changed

+121
-38
lines changed

contracts/contract/deposit/RocketDepositPool.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul
516516
RocketMegapoolDelegateInterface megapool = RocketMegapoolDelegateInterface(msg.sender);
517517
address nodeAddress = megapool.getNodeAddress();
518518
addUint(keccak256(abi.encodePacked("node.deposit.credit.balance", nodeAddress)), _amount);
519+
addUint("deposit.pool.node.balance", _amount);
519520
}
520521

521522
/// @notice Allows node operator to withdraw any ETH credit they have as rETH

contracts/contract/megapool/RocketMegapoolDelegate.sol

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDel
3737
event MegapoolValidatorStaked(uint256 indexed validatorId, uint256 time);
3838
event MegapoolDebtIncreased(uint256 amount, uint256 time);
3939
event MegapoolDebtReduced(uint256 amount, uint256 time);
40+
event MegapoolBondReduced(uint256 amount, uint256 time);
4041
event RewardsDistributed(uint256 nodeAmount, uint256 voterAmount, uint256 rethAmount, uint256 time);
4142
event RewardsClaimed(uint256 amount, uint256 time);
4243

@@ -138,6 +139,31 @@ contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDel
138139
emit MegapoolValidatorDequeued(_validatorId, block.timestamp);
139140
}
140141

142+
/// @notice Reduces this megapool's bond and applies credit if current bond exceeds requirement
143+
/// @param _amount Amount in ETH to reduce bond by
144+
function reduceBond(uint256 _amount) override external onlyMegapoolOwner {
145+
// Check pre-conditions
146+
require(_amount > 0, "Invalid amount");
147+
require(debt == 0, "Cannot reduce bond with debt");
148+
RocketNodeDepositInterface rocketNodeDeposit = RocketNodeDepositInterface(getContractAddress("rocketNodeDeposit"));
149+
uint256 newBondRequirement = rocketNodeDeposit.getBondRequirement(getActiveValidatorCount());
150+
require (nodeBond > newBondRequirement, "Bond is at minimum");
151+
uint256 maxReduce = nodeBond - newBondRequirement;
152+
require(_amount <= maxReduce, "New bond is too low");
153+
// Force distribute at previous capital ratio
154+
uint256 pendingRewards = getPendingRewards();
155+
if (pendingRewards > 0) {
156+
_distributeAmount(pendingRewards);
157+
}
158+
// Reduce node bond
159+
nodeBond -= _amount;
160+
// Apply credit
161+
RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool"));
162+
rocketDepositPool.applyCredit(_amount);
163+
// Emit event
164+
emit MegapoolBondReduced(_amount, block.timestamp);
165+
}
166+
141167
/// @notice Accepts requested funds from the deposit pool
142168
/// @param _validatorId the validator ID
143169
function assignFunds(uint32 _validatorId) external payable onlyLatestContract("rocketDepositPool", msg.sender) {

contracts/contract/megapool/RocketMegapoolPenalties.sol

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import {RocketNetworkSnapshotsInterface} from "../../interface/network/RocketNet
99
import {RocketMegapoolDelegateInterface} from "../../interface/megapool/RocketMegapoolDelegateInterface.sol";
1010
import {RocketMegapoolPenaltiesInterface} from "../../interface/megapool/RocketMegapoolPenaltiesInterface.sol";
1111

12-
/// @notice Performs deterministic deployment of megapool delegate contracts and handles deprecation of old ones
12+
/// @notice Applies penalties to megapools for MEV theft
1313
contract RocketMegapoolPenalties is RocketBase, RocketMegapoolPenaltiesInterface {
1414
// Constants
1515
uint256 constant internal penaltyMaximumPeriod = 50400;
16+
bytes32 constant internal penaltyKey = keccak256(abi.encodePacked("megapool.running.penalty"));
1617

1718
constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) {
1819
}
@@ -30,7 +31,9 @@ contract RocketMegapoolPenalties is RocketBase, RocketMegapoolPenaltiesInterface
3031
/// @param _megapool Address of the accused megapool
3132
/// @param _block Block that the theft occurred (used for uniqueness)
3233
/// @param _amount Amount in ETH of the penalty
33-
function penalise(address _megapool, uint256 _block, uint256 _amount) override external onlyTrustedNode(msg.sender) {
34+
function penalise(address _megapool, uint256 _block, uint256 _amount) override external onlyTrustedNode(msg.sender) onlyRegisteredMegapool(_megapool) {
35+
require(_amount > 0, "Invalid penalty amount");
36+
require(_block < block.number, "Invalid block number");
3437
// Check this penalty hasn't already reach majority and been applied
3538
bytes32 penaltyAppliedKey = keccak256(abi.encodePacked("megapool.penalty.submission.applied", _megapool, _block, _amount));
3639
require(!getBool(penaltyAppliedKey), "Penalty already applied");
@@ -52,41 +55,54 @@ contract RocketMegapoolPenalties is RocketBase, RocketMegapoolPenaltiesInterface
5255
}
5356
}
5457

55-
function getPenaltyRunningTotalAtBlock(address _megapool, uint32 _block) override external view returns (uint256) {
58+
/// @dev Manually execute a penalty that has hit majority vote
59+
/// @param _megapool Address of the accused megapool
60+
/// @param _block Block that the theft occurred (used for uniqueness)
61+
/// @param _amount Amount in ETH of the penalty
62+
function executePenalty(address _megapool, uint256 _block, uint256 _amount) override external {
63+
// Check this penalty hasn't already reach majority and been applied
64+
bytes32 penaltyAppliedKey = keccak256(abi.encodePacked("megapool.penalty.submission.applied", _megapool, _block, _amount));
65+
require(!getBool(penaltyAppliedKey), "Penalty already applied");
66+
// Get submission keys
67+
bytes32 submissionCountKey = keccak256(abi.encodePacked("megapool.penalty.submission", _megapool, _block, _amount));
68+
// Increment submission count
69+
uint256 submissionCount = getUint(submissionCountKey);
70+
// Check for majority
71+
RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted"));
72+
if (calcBase * submissionCount / rocketDAONodeTrusted.getMemberCount() > 0.5 ether) {
73+
// Apply penalty and mark as applied
74+
applyPenalty(_megapool, _amount);
75+
setBool(penaltyAppliedKey, true);
76+
}
77+
}
78+
79+
function getPenaltyRunningTotalAtBlock(uint32 _block) override external view returns (uint256) {
5680
RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots"));
57-
bytes32 penaltyKey = keccak256(abi.encodePacked("megapool.running.penalty", _megapool));
5881
return rocketNetworkSnapshots.lookup(penaltyKey, _block);
5982
}
6083

61-
function getCurrentPenaltyRunningTotal(address _megapool) override external view returns (uint256) {
84+
function getCurrentPenaltyRunningTotal() override external view returns (uint256) {
6285
RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots"));
63-
bytes32 penaltyKey = keccak256(abi.encodePacked("megapool.running.penalty", _megapool));
6486
(,,uint224 value) = rocketNetworkSnapshots.latest(penaltyKey);
6587
return uint256(value);
6688
}
6789

68-
function getCurrentMaxPenalty(address _megapool) override external view returns (uint256) {
90+
function getCurrentMaxPenalty() override external view returns (uint256) {
6991
// Get contracts
7092
RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots"));
7193
RocketDAOProtocolSettingsMegapoolInterface rocketDAOProtocolSettingsMegapool = RocketDAOProtocolSettingsMegapoolInterface(getContractAddress("rocketDAOProtocolSettingsMegapool"));
7294
// Grab max weekly penalty
7395
uint256 maxPenalty = rocketDAOProtocolSettingsMegapool.getMaximumEthPenalty();
74-
// Precompute storage key
75-
bytes32 penaltyKey = keccak256(abi.encodePacked("megapool.running.penalty", _megapool));
7696
// Get running total from 50400 slots ago
7797
uint256 earlierBlock = 0;
7898
if (block.number > penaltyMaximumPeriod) {
7999
earlierBlock = block.number - penaltyMaximumPeriod;
80100
}
81-
uint256 earlierRunningTotal = rocketNetworkSnapshots.lookupRecent(penaltyKey, uint32(earlierBlock), 5);
101+
uint256 earlierRunningTotal = uint256(rocketNetworkSnapshots.lookup(penaltyKey, uint32(earlierBlock)));
82102
// Get current running total
83-
(bool exists, uint32 slot, uint224 value) = rocketNetworkSnapshots.latest(penaltyKey);
84-
uint256 currentTotal = 0;
85-
if (exists) {
86-
currentTotal = value;
87-
}
103+
(bool exists,, uint224 currentTotal) = rocketNetworkSnapshots.latest(penaltyKey);
88104
// Cap the penalty at the maximum amount based on past 50400 blocks
89-
return maxPenalty - (currentTotal - earlierRunningTotal);
105+
return maxPenalty - (uint256(currentTotal) - earlierRunningTotal);
90106
}
91107

92108
/// @dev Applies a penalty up to given amount, honouring the max penalty parameter
@@ -96,29 +112,19 @@ contract RocketMegapoolPenalties is RocketBase, RocketMegapoolPenaltiesInterface
96112
RocketDAOProtocolSettingsMegapoolInterface rocketDAOProtocolSettingsMegapool = RocketDAOProtocolSettingsMegapoolInterface(getContractAddress("rocketDAOProtocolSettingsMegapool"));
97113
// Grab max weekly penalty
98114
uint256 maxPenalty = rocketDAOProtocolSettingsMegapool.getMaximumEthPenalty();
99-
// Precompute storage key
100-
bytes32 penaltyKey = keccak256(abi.encodePacked("megapool.running.penalty", _megapool));
101115
// Get running total from 50400 slots ago
102116
uint256 earlierBlock = 0;
103117
if (block.number > penaltyMaximumPeriod) {
104118
earlierBlock = block.number - penaltyMaximumPeriod;
105119
}
106-
uint256 earlierRunningTotal = rocketNetworkSnapshots.lookupRecent(penaltyKey, uint32(earlierBlock), 5);
120+
uint256 earlierRunningTotal = rocketNetworkSnapshots.lookup(penaltyKey, uint32(earlierBlock));
107121
// Get current running total
108-
(bool exists, uint32 slot, uint224 value) = rocketNetworkSnapshots.latest(penaltyKey);
109-
uint256 currentTotal = 0;
110-
if (exists) {
111-
currentTotal = value;
112-
}
122+
(,, uint224 currentTotal) = rocketNetworkSnapshots.latest(penaltyKey);
113123
// Cap the penalty at the maximum amount based on past 50400 blocks
114-
uint256 maxCurrentPenalty = maxPenalty - (currentTotal - earlierRunningTotal);
115-
if (_amount > maxCurrentPenalty) {
116-
_amount = maxCurrentPenalty;
117-
}
118-
// Prevent useless penalty
119-
require(_amount > 0, "No penalty to apply");
124+
uint256 maxCurrentPenalty = maxPenalty - (uint256(currentTotal) - earlierRunningTotal);
125+
require(_amount <= maxCurrentPenalty, "Max penalty exceeded");
120126
// Insert new running total
121-
rocketNetworkSnapshots.push(penaltyKey, uint224(currentTotal + _amount));
127+
rocketNetworkSnapshots.push(penaltyKey, currentTotal + uint224(_amount));
122128
// Call megapool to increase debt
123129
RocketMegapoolDelegateInterface(_megapool).applyPenalty(_amount);
124130
}

contracts/interface/megapool/RocketMegapoolDelegateInterface.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface RocketMegapoolDelegateInterface is RocketMegapoolDelegateBaseInterface
1212

1313
function newValidator(uint256 _bondAmount, bool _useExpressTicket, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) external;
1414
function dequeue(uint32 _validatorId) external;
15+
function reduceBond(uint256 _amount) external;
1516
function assignFunds(uint32 _validatorId) external payable;
1617
function stake(uint32 _validatorId, ValidatorProof calldata _proof) external;
1718
function dissolveValidator(uint32 _validatorId) external;

contracts/interface/megapool/RocketMegapoolPenaltiesInterface.sol

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ pragma solidity >0.5.0 <0.9.0;
44
interface RocketMegapoolPenaltiesInterface {
55
function getVoteCount(address _megapool, uint256 _block, uint256 _amount) external view returns (uint256);
66
function penalise(address _megapool, uint256 _block, uint256 _amount) external;
7-
function getPenaltyRunningTotalAtBlock(address _megapool, uint32 _block) external view returns (uint256);
8-
function getCurrentPenaltyRunningTotal(address _megapool) external view returns (uint256);
9-
function getCurrentMaxPenalty(address _megapool) external view returns (uint256);
7+
function executePenalty(address _megapool, uint256 _block, uint256 _amount) external;
8+
function getPenaltyRunningTotalAtBlock(uint32 _block) external view returns (uint256);
9+
function getCurrentPenaltyRunningTotal() external view returns (uint256);
10+
function getCurrentMaxPenalty() external view returns (uint256);
1011
}

test/megapool/megapool-tests.js

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ export default function() {
345345
await deployMegapool({ from: node });
346346

347347
await applyPenalty(megapool, 0n, '301'.ether, trustedNode1);
348-
await applyPenalty(megapool, 0n, '301'.ether, trustedNode2);
348+
await shouldRevert(applyPenalty(megapool, 0n, '301'.ether, trustedNode2), 'Max penalty exceeded', 'Max penalty exceeded');
349349
});
350350

351351
it(printTitle('trusted node', 'can apply another penalty only after 50400 blocks'), async () => {
@@ -357,14 +357,62 @@ export default function() {
357357
const megapoolDebtBefore = await megapool.getDebt();
358358
await helpers.mine(50397);
359359
await applyPenalty(megapool, 1n, '300'.ether, trustedNode1);
360-
await shouldRevert(applyPenalty(megapool, 1n, '300'.ether, trustedNode2), 'Applied greater penalty', 'No penalty to apply');
360+
await shouldRevert(applyPenalty(megapool, 1n, '300'.ether, trustedNode2), 'Applied greater penalty', 'Max penalty exceeded');
361361
await helpers.mine(3);
362362
await applyPenalty(megapool, 1n, '300'.ether, trustedNode2);
363363
const megapoolDebtAfter = await megapool.getDebt();
364364
const debtDelta = megapoolDebtAfter - megapoolDebtBefore;
365365
assertBN.equal(debtDelta, '300'.ether);
366366
});
367367

368+
snapshotDescribe('With overbonded megapool', () => {
369+
before(async () => {
370+
// Deposit enough for 4 validators
371+
await userDeposit({ from: random, value: ('32'.ether - '4'.ether) * 4n });
372+
await nodeDeposit(node, '4'.ether);
373+
await nodeDeposit(node, '4'.ether);
374+
await nodeDeposit(node, '4'.ether);
375+
// Reduce 'reduced.bond' to 2 ETH
376+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'reduced.bond', '2'.ether, { from: owner });
377+
// Node is now overbonded by 2 ETH
378+
});
379+
380+
it(printTitle('node', 'can not reduce bond below requirement'), async () => {
381+
await shouldRevert(megapool.reduceBond('3'.ether), 'Reduced bond below requirement', 'New bond is too low');
382+
});
383+
384+
it(printTitle('node', 'can partially reduce bond'), async () => {
385+
await megapool.reduceBond('1'.ether);
386+
});
387+
388+
it(printTitle('node', 'can not reduce bond while at minimum'), async () => {
389+
await megapool.reduceBond('2'.ether);
390+
await shouldRevert(megapool.reduceBond('1'.ether), 'Reduced bond below requirement', 'Bond is at minimum');
391+
});
392+
393+
it(printTitle('node', 'can not reduce bond with debt'), async () => {
394+
await applyPenalty(megapool, 0n, '1'.ether, trustedNode1);
395+
await applyPenalty(megapool, 0n, '1'.ether, trustedNode2);
396+
await shouldRevert(megapool.reduceBond('2'.ether), 'Reduced bond with debt', 'Cannot reduce bond with debt');
397+
});
398+
399+
it(printTitle('node', 'can reduce bond to new requirement and use credit for another validator'), async () => {
400+
await megapool.reduceBond('2'.ether);
401+
await nodeDeposit(node, '2'.ether, false, '2'.ether);
402+
});
403+
404+
it(printTitle('node', 'can reduce bond to new requirement and use credit to mint rETH'), async () => {
405+
await megapool.reduceBond('2'.ether);
406+
await withdrawCredit(node, '2'.ether);
407+
});
408+
409+
it(printTitle('node', 'can reduce bond to new requirement and use some credit for another validator and some for rETH'), async () => {
410+
await megapool.reduceBond('2'.ether);
411+
await withdrawCredit(node, '1'.ether);
412+
await nodeDeposit(node, '2'.ether, false, '1'.ether);
413+
});
414+
});
415+
368416
snapshotDescribe('With full deposit pool', () => {
369417
const dissolvePeriod = (60 * 60 * 48); // 24 hours
370418

test/megapool/scenario-apply-penalty.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export async function applyPenalty(megapool, slot, amount, trustedNode) {
1616
let [voteCount, nodeDebt, currentMaxPenalty, currentPenaltyRunningTotal] = await Promise.all([
1717
rocketMegapoolPenalties.getVoteCount(megapool.target, slot, amount),
1818
megapool.getDebt(),
19-
rocketMegapoolPenalties.getCurrentMaxPenalty(megapool.target),
20-
rocketMegapoolPenalties.getCurrentPenaltyRunningTotal(megapool.target),
19+
rocketMegapoolPenalties.getCurrentMaxPenalty(),
20+
rocketMegapoolPenalties.getCurrentPenaltyRunningTotal(),
2121
]);
2222
return { voteCount, nodeDebt, currentMaxPenalty, currentPenaltyRunningTotal };
2323
}

0 commit comments

Comments
 (0)