Skip to content

Commit 5a389d4

Browse files
committed
Refactor bond reduction logic out of delegate
1 parent 602ed4a commit 5a389d4

18 files changed

+320
-111
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// SPDX-License-Identifier: GPL-3.0-only
2+
pragma solidity 0.7.6;
3+
pragma abicoder v2;
4+
5+
import "@openzeppelin/contracts/math/SafeMath.sol";
6+
7+
import "../RocketBase.sol";
8+
import "./RocketMinipoolDelegate.sol";
9+
import "../../interface/minipool/RocketMinipoolBondReducerInterface.sol";
10+
11+
/// @notice Handles bond reduction window and trusted node cancellation
12+
contract RocketMinipoolBondReducer is RocketBase, RocketMinipoolBondReducerInterface {
13+
14+
// Libs
15+
using SafeMath for uint;
16+
17+
// Events
18+
event BeginBondReduction(address indexed minipool, uint256 time);
19+
event CancelReductionVoted(address indexed minipool, address indexed member, uint256 time);
20+
event ReductionCancelled(address indexed minipool, uint256 time);
21+
22+
constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) {
23+
version = 1;
24+
}
25+
26+
/// @notice Flags a minipool as wanting to reduce collateral, owner can then call `reduceBondAmount` once waiting
27+
/// period has elapsed
28+
/// @param _minipoolAddress Address of the minipool
29+
function beginReduceBondAmount(address _minipoolAddress) override external onlyLatestContract("rocketMinipoolBondReducer", address(this)) {
30+
RocketMinipoolDelegate minipool = RocketMinipoolDelegate(_minipoolAddress);
31+
require(msg.sender == minipool.getNodeAddress(), "Only minipool owner");
32+
// Check if has been previously cancelled
33+
bool reductionCancelled = getBool(keccak256(abi.encodePacked("minipool.bond.reduction.cancelled", address(minipool))));
34+
require(!reductionCancelled, "This minipool is not allowed to reduce bond");
35+
require(minipool.getStatus() == MinipoolStatus.Staking, "Minipool must be staking");
36+
setUint(keccak256(abi.encodePacked("minipool.bond.reduction.time", _minipoolAddress)), block.timestamp);
37+
emit BeginBondReduction(_minipoolAddress, block.timestamp);
38+
}
39+
40+
/// @notice Returns whether owner of given minipool can reduce bond amount given the waiting period constraint
41+
/// @param _minipoolAddress Address of the minipool
42+
function canReduceBondAmount(address _minipoolAddress) override public view returns (bool) {
43+
RocketMinipoolDelegate minipool = RocketMinipoolDelegate(_minipoolAddress);
44+
RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress("rocketDAONodeTrustedSettingsMinipool"));
45+
uint256 reduceBondTime = getUint(keccak256(abi.encodePacked("minipool.bond.reduction.time", _minipoolAddress)));
46+
return rocketDAONodeTrustedSettingsMinipool.isWithinBondReductionWindow(block.timestamp.sub(reduceBondTime));
47+
}
48+
49+
/// @notice Can be called by trusted nodes to cancel a reduction in bond if the validator has too low of a balance
50+
/// @param _minipoolAddress Address of the minipool
51+
function voteCancelReduction(address _minipoolAddress) override external onlyTrustedNode(msg.sender) onlyLatestContract("rocketMinipoolBondReducer", address(this)) {
52+
RocketMinipoolDelegate minipool = RocketMinipoolDelegate(_minipoolAddress);
53+
// Get contracts
54+
RocketDAONodeTrustedInterface rocketDAONode = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted"));
55+
// Check for multiple votes
56+
bytes32 memberVotedKey = keccak256(abi.encodePacked("minipool.bond.reduction.member.voted", _minipoolAddress, msg.sender));
57+
bool memberVoted = getBool(memberVotedKey);
58+
require(!memberVoted, "Member has already voted to cancel");
59+
setBool(memberVotedKey, true);
60+
// Emit event
61+
emit CancelReductionVoted(_minipoolAddress, msg.sender, block.timestamp);
62+
// Check if required quorum has voted
63+
RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress("rocketDAONodeTrustedSettingsMinipool"));
64+
uint256 quorum = rocketDAONode.getMemberCount().mul(rocketDAONodeTrustedSettingsMinipool.getCancelBondReductionQuorum()).div(calcBase);
65+
bytes32 totalCancelVotesKey = keccak256(abi.encodePacked("minipool.bond.reduction.vote.count", _minipoolAddress));
66+
uint256 totalCancelVotes = getUint(totalCancelVotesKey).add(1);
67+
if (totalCancelVotes > quorum) {
68+
// Emit event
69+
emit ReductionCancelled(_minipoolAddress, block.timestamp);
70+
setBool(keccak256(abi.encodePacked("minipool.bond.reduction.cancelled", _minipoolAddress)), true);
71+
deleteUint(keccak256(abi.encodePacked("minipool.bond.reduction.time", _minipoolAddress)));
72+
} else {
73+
// Increment total
74+
setUint(totalCancelVotesKey, totalCancelVotes);
75+
}
76+
}
77+
78+
/// @notice Called by minipools when they are reducing bond to handle state changes outside the minipool
79+
/// @param _from The previous bond amount
80+
/// @param _to The new bond amount
81+
function reduceBondAmount(uint256 _from, uint256 _to) override external onlyRegisteredMinipool(msg.sender) onlyLatestContract("rocketMinipoolBondReducer", address(this)) {
82+
// Check if has been cancelled
83+
bool reductionCancelled = getBool(keccak256(abi.encodePacked("minipool.bond.reduction.cancelled", address(msg.sender))));
84+
require(!reductionCancelled, "This minipool is not allowed to reduce bond");
85+
// Check wait period is satisfied
86+
require(canReduceBondAmount(msg.sender), "Wait period not satisfied");
87+
// Get contracts
88+
RocketNodeDepositInterface rocketNodeDeposit = RocketNodeDepositInterface(getContractAddress("rocketNodeDeposit"));
89+
RocketMinipoolDelegate minipool = RocketMinipoolDelegate(msg.sender);
90+
// Check the new bond amount is valid
91+
require(rocketNodeDeposit.isValidDepositAmount(_to), "Invalid bond amount");
92+
// Calculate difference
93+
uint256 delta = _from.sub(_to);
94+
// Get node address
95+
address nodeAddress = minipool.getNodeAddress();
96+
// Increase ETH matched or revert if exceeds limit based on current RPL stake
97+
rocketNodeDeposit.increaseEthMatched(nodeAddress, delta);
98+
// Increase node operator's deposit credit
99+
rocketNodeDeposit.increaseDepositCreditBalance(nodeAddress, delta);
100+
}
101+
}

contracts/contract/minipool/RocketMinipoolDelegate.sol

Lines changed: 24 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import "../../interface/token/RocketTokenRETHInterface.sol";
2323
import "../../types/MinipoolDeposit.sol";
2424
import "../../types/MinipoolStatus.sol";
2525
import "../../interface/node/RocketNodeDepositInterface.sol";
26+
import "../../interface/minipool/RocketMinipoolBondReducerInterface.sol";
2627

2728
/// @notice Provides the logic for each individual minipool in the Rocket Pool network
2829
/// @dev Minipools exclusively DELEGATECALL into this contract it is never called directly
@@ -41,9 +42,6 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
4142
// Events
4243
event StatusUpdated(uint8 indexed status, uint256 time);
4344
event ScrubVoted(address indexed member, uint256 time);
44-
event BeginBondReduction(uint256 time);
45-
event CancelReductionVoted(address indexed member, uint256 time);
46-
event ReductionCancelled(uint256 time);
4745
event BondReduced(uint256 previousBondAmount, uint256 newBondAmount, uint256 time);
4846
event MinipoolScrubbed(uint256 time);
4947
event MinipoolPrestaked(bytes validatorPubkey, bytes validatorSignature, bytes32 depositDataRoot, uint256 amount, bytes withdrawalCredentials, uint256 time);
@@ -270,8 +268,13 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
270268
/// @dev Sets the bond value and vacancy flag on this minipool
271269
/// @param _bondAmount The bond amount selected by the node operator
272270
function prepareVacancy(uint256 _bondAmount) override external onlyLatestContract("rocketMinipoolManager", msg.sender) onlyInitialised {
271+
// Get contracts
272+
RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool"));
273273
// Store bond amount
274274
nodeDepositBalance = _bondAmount;
275+
// Calculate user amount from launch amount
276+
uint256 launchAmount = rocketDAOProtocolSettingsMinipool.getLaunchBalance();
277+
userDepositBalance = uint256(32 ether).sub(nodeDepositBalance);
275278
// Flag as vacant
276279
vacant = true;
277280
// Set status to preLaunch
@@ -284,7 +287,6 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
284287
require(status == MinipoolStatus.Prelaunch, "The minipool can only promote while in prelaunch");
285288
require(vacant, "Cannot promote a non-vacant minipool");
286289
// Get contracts
287-
RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool"));
288290
RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress("rocketDAONodeTrustedSettingsMinipool"));
289291
// Clear vacant flag
290292
vacant = false;
@@ -293,9 +295,6 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
293295
require(block.timestamp > statusTime + scrubPeriod, "Not enough time has passed to promote");
294296
// Progress to staking
295297
setStatus(MinipoolStatus.Staking);
296-
// Calculate user amount from launch amount
297-
uint256 launchAmount = rocketDAOProtocolSettingsMinipool.getLaunchBalance();
298-
userDepositBalance = uint256(32 ether).sub(nodeDepositBalance);
299298
// Increment node's number of staking minipools
300299
RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager"));
301300
rocketMinipoolManager.incrementNodeStakingMinipoolCount(nodeAddress);
@@ -528,10 +527,7 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
528527
// Load contracts
529528
RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool"));
530529
// Check if being dissolved by minipool owner or minipool is timed out
531-
require(
532-
(status == MinipoolStatus.Prelaunch && block.timestamp.sub(statusTime) >= rocketDAOProtocolSettingsMinipool.getLaunchTimeout()),
533-
"The minipool can only be dissolved once it has timed out"
534-
);
530+
require(block.timestamp.sub(statusTime) >= rocketDAOProtocolSettingsMinipool.getLaunchTimeout(), "The minipool can only be dissolved once it has timed out");
535531
// Perform the dissolution
536532
_dissolve();
537533
}
@@ -547,6 +543,8 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
547543
require(rocketMinipoolManager.getMinipoolExists(address(this)), "Minipool already closed");
548544
rocketMinipoolManager.destroyMinipool();
549545
// Clear state
546+
nodeDepositBalance = 0;
547+
nodeRefundBalance = 0;
550548
userDepositBalance = 0;
551549
userDepositBalanceLegacy = 0;
552550
userDepositAssignedTime = 0;
@@ -594,70 +592,23 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
594592
}
595593
}
596594

597-
/// @notice Flags this minipool as wanting to reduce collateral, owner can then call `reduceBondAmount` once waiting
598-
/// period has elapsed
599-
function beginReduceBondAmount() override external onlyMinipoolOwner(msg.sender) onlyInitialised {
600-
require(!reductionCancelled, "This minipool is allowed to reduce bond");
601-
require(status == MinipoolStatus.Staking, "Minipool must be staking");
602-
reduceBondTime = block.timestamp;
603-
emit BeginBondReduction(block.timestamp);
604-
}
605-
606-
/// @notice Returns whether owner can reduce bond amount given the waiting period constraint
607-
function canReduceBondAmount() override public view returns (bool) {
608-
RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress("rocketDAONodeTrustedSettingsMinipool"));
609-
return rocketDAONodeTrustedSettingsMinipool.isWithinBondReductionWindow(block.timestamp.sub(reduceBondTime));
610-
}
611-
612-
/// @notice Can be called by trusted nodes to cancel a reduction in bond if the validator has too low of a balance
613-
function voteCancelReduction() override external onlyInitialised {
614-
require(!memberCancelVotes[msg.sender], "Member has already voted to cancel");
615-
// Must be a trusted member
616-
RocketDAONodeTrustedInterface rocketDAONode = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted"));
617-
require(rocketDAONode.getMemberIsValid(msg.sender), "Not a trusted member");
618-
memberCancelVotes[msg.sender] = true;
619-
// Emit event
620-
emit CancelReductionVoted(msg.sender, block.timestamp);
621-
// Check if required quorum has voted
622-
RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress("rocketDAONodeTrustedSettingsMinipool"));
623-
uint256 quorum = rocketDAONode.getMemberCount().mul(rocketDAONodeTrustedSettingsMinipool.getCancelBondReductionQuorum()).div(calcBase);
624-
if (totalCancelVotes.add(1) > quorum) {
625-
// Emit event
626-
emit ReductionCancelled(block.timestamp);
627-
reductionCancelled = true;
628-
reduceBondTime = 0;
629-
} else {
630-
// Increment total
631-
totalCancelVotes = totalCancelVotes.add(1);
632-
}
633-
}
634-
635595
/// @notice Reduces the ETH bond amount and credits the owner the difference
636596
/// @param _amount The amount to reduce the bond to (e.g. 8 ether)
637-
function reduceBondAmount(uint256 _amount) external onlyMinipoolOwner(msg.sender) onlyInitialised {
597+
function reduceBondAmount(uint256 _amount) override external onlyMinipoolOwner(msg.sender) onlyInitialised {
638598
uint256 previousBond = nodeDepositBalance;
639-
require(canReduceBondAmount(), "Wait period not satisfied");
640-
require(!reductionCancelled, "This minipool is allowed to reduce bond");
641599
require(_amount < previousBond, "Bond must be lower than current amount");
642600
require(status == MinipoolStatus.Staking, "Minipool must be staking");
643-
// Get contracts
644-
RocketNodeDepositInterface rocketNodeDeposit = RocketNodeDepositInterface(getContractAddress("rocketNodeDeposit"));
645-
// Check the new bond amount is valid
646-
require(rocketNodeDeposit.isValidDepositAmount(_amount), "Invalid bond amount");
647601
// Distribute any skimmed rewards
648602
distributeSkimmedRewards();
649-
// Calculate bond difference
650-
uint256 delta = previousBond.sub(_amount);
651-
// Increase ETH matched or revert if exceeds limit based on current RPL stake
652-
rocketNodeDeposit.increaseEthMatched(nodeAddress, delta);
603+
// Approve reduction and handle external state changes
604+
RocketMinipoolBondReducerInterface rocketBondReducer = RocketMinipoolBondReducerInterface(getContractAddress("rocketMinipoolBondReducer"));
605+
rocketBondReducer.reduceBondAmount(previousBond, _amount);
653606
// Update user/node balances
654-
userDepositBalance = getUserDepositBalance().add(delta);
607+
userDepositBalance = getUserDepositBalance().add(previousBond.sub(_amount));
655608
nodeDepositBalance = _amount;
656609
// Reset node fee to current network rate
657610
RocketNetworkFeesInterface rocketNetworkFees = RocketNetworkFeesInterface(getContractAddress("rocketNetworkFees"));
658611
nodeFee = rocketNetworkFees.getNodeFee();
659-
// Increase node operator's deposit credit
660-
rocketNodeDeposit.increaseDepositCreditBalance(nodeAddress, delta);
661612
// Break state to prevent rollback exploit
662613
if (depositType != MinipoolDeposit.Variable) {
663614
userDepositBalanceLegacy = 2**256-1;
@@ -724,14 +675,16 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
724675
RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager"));
725676
rocketMinipoolManager.removeVacantMinipool();
726677
} else {
727-
// Transfer user balance to deposit pool
728-
uint256 userCapital = getUserDepositBalance();
729-
rocketDepositPool.recycleDissolvedDeposit{value : userCapital}();
730-
// Emit ether withdrawn event
731-
emit EtherWithdrawn(address(rocketDepositPool), userCapital, block.timestamp);
678+
if (depositType == MinipoolDeposit.Full) {
679+
// Handle legacy Full type minipool
680+
rocketMinipoolQueue.removeMinipool(MinipoolDeposit.full);
681+
} else {
682+
// Transfer user balance to deposit pool
683+
uint256 userCapital = getUserDepositBalance();
684+
rocketDepositPool.recycleDissolvedDeposit{value : userCapital}();
685+
// Emit ether withdrawn event
686+
emit EtherWithdrawn(address(rocketDepositPool), userCapital, block.timestamp);
687+
}
732688
}
733-
// Clear storage
734-
nodeDepositBalance = 0;
735-
nodeRefundBalance = 0;
736689
}
737690
}

contracts/contract/minipool/RocketMinipoolManager.sol

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import "../../interface/node/RocketNodeDistributorInterface.sol";
2525
import "../../interface/network/RocketNetworkPenaltiesInterface.sol";
2626
import "../../interface/minipool/RocketMinipoolPenaltyInterface.sol";
2727
import "../../interface/node/RocketNodeDepositInterface.sol";
28+
import "./RocketMinipoolDelegate.sol";
2829

2930
/// @notice Minipool creation, removal and management
3031
contract RocketMinipoolManager is RocketBase, RocketMinipoolManagerInterface {
@@ -35,6 +36,9 @@ contract RocketMinipoolManager is RocketBase, RocketMinipoolManagerInterface {
3536
// Events
3637
event MinipoolCreated(address indexed minipool, address indexed node, uint256 time);
3738
event MinipoolDestroyed(address indexed minipool, address indexed node, uint256 time);
39+
event BeginBondReduction(address indexed minipool, uint256 time);
40+
event CancelReductionVoted(address indexed minipool, address indexed member, uint256 time);
41+
event ReductionCancelled(address indexed minipool, uint256 time);
3842

3943
constructor(RocketStorageInterface _rocketStorageAddress) RocketBase(_rocketStorageAddress) {
4044
version = 3;
@@ -413,7 +417,12 @@ contract RocketMinipoolManager is RocketBase, RocketMinipoolManagerInterface {
413417
if (ethMatched == 0) {
414418
ethMatched = getNodeActiveMinipoolCount(nodeAddress).mul(16 ether);
415419
}
416-
ethMatched = ethMatched.sub(minipool.getUserDepositBalance());
420+
// Handle legacy minipools
421+
if (minipool.getDepositType() == MinipoolDeposit.Variable) {
422+
ethMatched = ethMatched.sub(minipool.getUserDepositBalance());
423+
} else {
424+
ethMatched = ethMatched.sub(16 ether);
425+
}
417426
setUint(keccak256(abi.encodePacked("eth.matched.node.amount", nodeAddress)), ethMatched);
418427
// Update minipool data
419428
setBool(keccak256(abi.encodePacked("minipool.exists", msg.sender)), false);
@@ -492,5 +501,4 @@ contract RocketMinipoolManager is RocketBase, RocketMinipoolManagerInterface {
492501
details.penaltyRate = rocketMinipoolPenalty.getPenaltyRate(_minipoolAddress);
493502
return details;
494503
}
495-
496504
}

contracts/contract/minipool/RocketMinipoolStorageLayout.sol

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,6 @@ abstract contract RocketMinipoolStorageLayout {
7171
uint256 internal preLaunchValue;
7272
uint256 internal userDepositBalance;
7373

74-
// Bond reduction state
75-
uint256 internal reduceBondTime;
76-
mapping(address => bool) internal memberCancelVotes;
77-
uint256 internal totalCancelVotes;
78-
bool internal reductionCancelled;
79-
8074
// Vacant minipool
8175
bool internal vacant;
8276

0 commit comments

Comments
 (0)