RocketNodeStaking - Node operators can reduce slashing impact by withdrawing excess staked RPL
Oracle nodes update the Minipools' balance and progress it to the withdrawable state when they observe the minipools stake to become withdrawable. If the observed stakingEndBalance is less than the user deposit for that pool, the node operator is punished for the difference.\\n```\\nrocketMinipoolManager.setMinipoolWithdrawalBalances(\\_minipoolAddress, \\_stakingEndBalance, nodeAmount);\\n// Apply node penalties by liquidating RPL stake\\nif (\\_stakingEndBalance < userDepositBalance) {\\n    RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking"));\\n    rocketNodeStaking.slashRPL(minipool.getNodeAddress(), userDepositBalance - \\_stakingEndBalance);\\n}\\n```\\n\\nThe amount slashed is at max `userDepositBalance - stakingEndBalance`. The `userDepositBalance` is at least `16 ETH` (minipool.half/.full) and at max `32 ETH` (minipool.empty). The maximum amount to be slashed is therefore `32 ETH` (endBalance = 0, minipool.empty).\\nThe slashing amount is denoted in `ETH`. The `RPL` price (in ETH) is updated regularly by oracle nodes (see related issue https://github.com/ConsenSys/rocketpool-audit-2021-03/issues/32; note that the `RPL` token is potentially affected by a similar issue as one can stake `RPL`, wait for the cooldown period & wait for the price to change, and withdraw stake at higher `RPL` price/ETH). The `ETH` amount to be slashed is converted to `RPL`, and the corresponding `RPL` stake is slashed.\\n```\\nuint256 rplSlashAmount = calcBase.mul(\\_ethSlashAmount).div(rocketNetworkPrices.getRPLPrice());\\n// Cap slashed amount to node's RPL stake\\nuint256 rplStake = getNodeRPLStake(\\_nodeAddress);\\nif (rplSlashAmount > rplStake) { rplSlashAmount = rplStake; }\\n// Transfer slashed amount to auction contract\\nrocketVault.transferToken("rocketAuctionManager", getContractAddress("rocketTokenRPL"), rplSlashAmount);\\n// Update RPL stake amounts\\ndecreaseTotalRPLStake(rplSlashAmount);\\ndecreaseNodeRPLStake(\\_nodeAddress, rplSlashAmount);\\n```\\n\\nIf the node does not have a sufficient `RPL` stake to cover the losses, the slashing amount is capped at whatever amount of `RPL` the node has left staked.\\nThe minimum amount of `RPL` a node needs to have staked if it operates minipools is calculated as follows:\\n```\\n    // Calculate minimum RPL stake\\n    return rocketDAOProtocolSettingsMinipool.getHalfDepositUserAmount()\\n        .mul(rocketDAOProtocolSettingsNode.getMinimumPerMinipoolStake())\\n        .mul(rocketMinipoolManager.getNodeMinipoolCount(\\_nodeAddress))\\n        .div(rocketNetworkPrices.getRPLPrice());\\n}\\n```\\n\\nWith the current configuration, this would resolve in a minimum stake of `16 ETH * 0.1 (10% collateralization) * 1 (nr_minipools) * RPL_Price` for a node operating 1 minipool. This means a node operator basically only needs to have 10% of `16 ETH` staked to operate one minipool.\\nAn operator can withdraw their stake at any time, but they have to wait at least 14 days after the last time they staked (cooldown period). They can, at max, withdraw all but the minimum stake required to run the pools (nr_of_minipools * 16 ETH * 10%). This also means that after the cooldown period, they can reduce their stake to 10% of the half deposit amount (16ETH), then perform a voluntary exit on ETH2 so that the minipool becomes `withdrawable`. If they end up with less than the `userDepositBalance` in staking rewards, they would only get slashed the `1.6 ETH` at max (10% of 16ETH half deposit amount for 1 minipool) even though they incurred a loss that may be up to 32 ETH (empty Minipool empty amount).\\nFurthermore, if a node operator runs multiple minipools, let's say 5, then they would have to provide at least `5*16ETH*0.1 = 8ETH` as a security guarantee in the form of staked RPL. If the node operator incurs a loss with one of their minipools, their 8 ETH RPL stake will likely be slashed in full. Their other - still operating - minipools are not backed by any RPL anymore, and they effectively cannot be slashed anymore. This means that a malicious node operator can create multiple minipools, stake the minimum amount of RPL, get slashed for one minipool, and still operate the others without having the minimum RPL needed to run the minipools staked (getNodeMinipoolLimit).\\nThe RPL stake is donated to the RocketAuctionManager, where they can attempt to buy back RPL potentially at a discount.\\nNote: Staking more RPL (e.g., to add another Minipool) resets the cooldown period for the total RPL staked (not only for the newly added)
It is recommended to redesign the withdrawal process to prevent users from withdrawing their stake while slashable actions can still occur. A potential solution may be to add a locking period in the process. A node operator may schedule the withdrawal of funds, and after a certain time has passed, may withdraw them. This prevents the immediate withdrawal of funds that may need to be reduced while slashable events can still occur. E.g.:\\nA node operator requests to withdraw all but the minimum required stake to run their pools.\\nThe funds are scheduled for withdrawal and locked until a period of X days has passed.\\n(optional) In this period, a slashable event occurs. The funds for compensation are taken from the user's stake including the funds scheduled for withdrawal.\\nAfter the time has passed, the node operator may call a function to trigger the withdrawal and get paid out.
```\\nrocketMinipoolManager.setMinipoolWithdrawalBalances(\\_minipoolAddress, \\_stakingEndBalance, nodeAmount);\\n// Apply node penalties by liquidating RPL stake\\nif (\\_stakingEndBalance < userDepositBalance) {\\n    RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking"));\\n    rocketNodeStaking.slashRPL(minipool.getNodeAddress(), userDepositBalance - \\_stakingEndBalance);\\n}\\n```\\n