Skip to content

Commit

Permalink
Prevent vacant minipool from refunding
Browse files Browse the repository at this point in the history
  • Loading branch information
kanewallmann committed Mar 13, 2023
1 parent 8b995d0 commit ec3c5fa
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 32 deletions.
87 changes: 55 additions & 32 deletions contracts/contract/minipool/RocketMinipoolDelegate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,39 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
event EtherWithdrawalProcessed(address indexed executed, uint256 nodeAmount, uint256 userAmount, uint256 totalBalance, uint256 time);

// Status getters
function getStatus() override external view returns (MinipoolStatus) { return status; }
function getFinalised() override external view returns (bool) { return finalised; }
function getStatusBlock() override external view returns (uint256) { return statusBlock; }
function getStatusTime() override external view returns (uint256) { return statusTime; }
function getScrubVoted(address _member) override external view returns (bool) { return memberScrubVotes[_member]; }
function getStatus() override external view returns (MinipoolStatus) {return status;}

function getFinalised() override external view returns (bool) {return finalised;}

function getStatusBlock() override external view returns (uint256) {return statusBlock;}

function getStatusTime() override external view returns (uint256) {return statusTime;}

function getScrubVoted(address _member) override external view returns (bool) {return memberScrubVotes[_member];}

// Deposit type getter
function getDepositType() override external view returns (MinipoolDeposit) { return depositType; }
function getDepositType() override external view returns (MinipoolDeposit) {return depositType;}

// Node detail getters
function getNodeAddress() override external view returns (address) { return nodeAddress; }
function getNodeFee() override external view returns (uint256) { return nodeFee; }
function getNodeDepositBalance() override external view returns (uint256) { return nodeDepositBalance; }
function getNodeRefundBalance() override external view returns (uint256) { return nodeRefundBalance; }
function getNodeDepositAssigned() override external view returns (bool) { return userDepositAssignedTime != 0; }
function getPreLaunchValue() override external view returns (uint256) { return preLaunchValue; }
function getNodeTopUpValue() override external view returns (uint256) { return nodeDepositBalance.sub(preLaunchValue); }
function getVacant() override external view returns (bool) { return vacant; }
function getPreMigrationBalance() override external view returns (uint256) { return preMigrationBalance; }
function getUserDistributed() override external view returns (bool) { return userDistributed; }
function getNodeAddress() override external view returns (address) {return nodeAddress;}

function getNodeFee() override external view returns (uint256) {return nodeFee;}

function getNodeDepositBalance() override external view returns (uint256) {return nodeDepositBalance;}

function getNodeRefundBalance() override external view returns (uint256) {return nodeRefundBalance;}

function getNodeDepositAssigned() override external view returns (bool) {return userDepositAssignedTime != 0;}

function getPreLaunchValue() override external view returns (uint256) {return preLaunchValue;}

function getNodeTopUpValue() override external view returns (uint256) {return nodeDepositBalance.sub(preLaunchValue);}

function getVacant() override external view returns (bool) {return vacant;}

function getPreMigrationBalance() override external view returns (uint256) {return preMigrationBalance;}

function getUserDistributed() override external view returns (bool) {return userDistributed;}

// User deposit detail getters
function getUserDepositBalance() override public view returns (uint256) {
Expand All @@ -76,9 +89,12 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
return userDepositBalanceLegacy;
}
}
function getUserDepositAssigned() override external view returns (bool) { return userDepositAssignedTime != 0; }
function getUserDepositAssignedTime() override external view returns (uint256) { return userDepositAssignedTime; }
function getTotalScrubVotes() override external view returns (uint256) { return totalScrubVotes; }

function getUserDepositAssigned() override external view returns (bool) {return userDepositAssignedTime != 0;}

function getUserDepositAssignedTime() override external view returns (uint256) {return userDepositAssignedTime;}

function getTotalScrubVotes() override external view returns (uint256) {return totalScrubVotes;}

/// @dev Prevent direct calls to this contract
modifier onlyInitialised() {
Expand Down Expand Up @@ -180,7 +196,7 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
require(status >= MinipoolStatus.Initialised && status <= MinipoolStatus.Staking, "The user deposit can only be assigned while initialised, in prelaunch, or staking");
require(userDepositAssignedTime == 0, "The user deposit has already been assigned");
// Progress initialised minipool to prelaunch
if (status == MinipoolStatus.Initialised) { setStatus(MinipoolStatus.Prelaunch); }
if (status == MinipoolStatus.Initialised) {setStatus(MinipoolStatus.Prelaunch);}
// Update user deposit details
userDepositBalance = msg.value;
userDepositAssignedTime = block.timestamp;
Expand All @@ -196,6 +212,8 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn

/// @notice Refund node ETH refinanced from user deposited ETH
function refund() override external onlyMinipoolOwnerOrWithdrawalAddress(msg.sender) onlyInitialised {
// Prevent vacant minipools from calling
require(vacant == false, "Vacant minipool cannot refund");
// Check refund balance
require(nodeRefundBalance > 0, "No amount of the node deposit is available for refund");
// If this minipool was distributed by a user, force finalisation on the node operator
Expand Down Expand Up @@ -417,7 +435,7 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
function beginUserDistribute() override external onlyInitialised {
require(status == MinipoolStatus.Staking, "Minipool must be staking");
uint256 totalBalance = address(this).balance.sub(nodeRefundBalance);
require (totalBalance >= 8 ether, "Balance too low");
require(totalBalance >= 8 ether, "Balance too low");
// Prevent calls resetting distribute time before window has passed
RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool"));
uint256 timeElapsed = block.timestamp.sub(userDistributeTime);
Expand All @@ -432,7 +450,7 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool"));
// Calculate if time elapsed since call to `beginUserDistribute` is within the allowed window
uint256 timeElapsed = block.timestamp.sub(userDistributeTime);
return(rocketDAOProtocolSettingsMinipool.isWithinUserDistributeWindow(timeElapsed));
return (rocketDAOProtocolSettingsMinipool.isWithinUserDistributeWindow(timeElapsed));
}

/// @notice Allows the owner of this minipool to finalise it after a user has manually distributed the balance
Expand Down Expand Up @@ -611,7 +629,7 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
uint256 quorum = rocketDAONode.getMemberCount().mul(rocketDAONodeTrustedSettingsMinipool.getScrubQuorum()).div(calcBase);
if (totalScrubVotes.add(1) > quorum) {
// Slash RPL equal to minimum stake amount (if enabled)
if (!vacant && rocketDAONodeTrustedSettingsMinipool.getScrubPenaltyEnabled()){
if (!vacant && rocketDAONodeTrustedSettingsMinipool.getScrubPenaltyEnabled()) {
RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking"));
RocketDAOProtocolSettingsNodeInterface rocketDAOProtocolSettingsNode = RocketDAOProtocolSettingsNodeInterface(getContractAddress("rocketDAOProtocolSettingsNode"));
RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool"));
Expand All @@ -620,9 +638,9 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
// In prelaunch userDepositBalance hasn't been set so we calculate it as 32 ETH - bond amount
rocketNodeStaking.slashRPL(
nodeAddress,
launchAmount.sub(nodeDepositBalance)
.mul(rocketDAOProtocolSettingsNode.getMinimumPerMinipoolStake())
.div(calcBase)
launchAmount.sub(nodeDepositBalance)
.mul(rocketDAOProtocolSettingsNode.getMinimumPerMinipoolStake())
.div(calcBase)
);
}
// Dissolve this minipool, recycling ETH back to deposit pool
Expand All @@ -646,20 +664,25 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
// Approve reduction and handle external state changes
RocketMinipoolBondReducerInterface rocketBondReducer = RocketMinipoolBondReducerInterface(getContractAddress("rocketMinipoolBondReducer"));
uint256 previousBond = nodeDepositBalance;
uint256 newBondAmount = rocketBondReducer.reduceBondAmount();
uint256 newBond = rocketBondReducer.reduceBondAmount();
// Update user/node balances
userDepositBalance = getUserDepositBalance().add(previousBond.sub(newBondAmount));
nodeDepositBalance = newBondAmount;
userDepositBalance = getUserDepositBalance().add(previousBond.sub(newBond));
nodeDepositBalance = newBond;
// Reset node fee to current network rate
RocketNetworkFeesInterface rocketNetworkFees = RocketNetworkFeesInterface(getContractAddress("rocketNetworkFees"));
nodeFee = rocketNetworkFees.getNodeFee();
uint256 prevFee = nodeFee;
uint256 newFee = rocketNetworkFees.getNodeFee();
nodeFee = newFee;
// Update staking minipool counts and fee numerator
RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager"));
rocketMinipoolManager.updateNodeStakingMinipoolCount(previousBond, newBond, prevFee, newFee);
// Break state to prevent rollback exploit
if (depositType != MinipoolDeposit.Variable) {
userDepositBalanceLegacy = 2**256-1;
userDepositBalanceLegacy = 2 ** 256 - 1;
depositType = MinipoolDeposit.Variable;
}
// Emit event
emit BondReduced(previousBond, newBondAmount, block.timestamp);
emit BondReduced(previousBond, newBond, block.timestamp);
}

/// @dev Distributes the current contract balance based on capital ratio and node fee
Expand Down
8 changes: 8 additions & 0 deletions test/minipool/minipool-vacant-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ export default function() {
});


it(printTitle('node operator', 'cannot call refund while vacant'), async () => {
// Create a vacant minipool with current balance of 33
let minipool = await createVacantMinipool('8'.ether, {from: node}, null, '33'.ether);
// Try to refund
await shouldRevert(refund(minipool, { from: node }), 'Was able to refund', 'Vacant minipool cannot refund');
});


it(printTitle('node operator', 'can not create a vacant minipool with an existing pubkey'), async () => {
// Create minipool with a pubkey
const pubkey = getValidatorPubkey();
Expand Down

0 comments on commit ec3c5fa

Please sign in to comment.