Skip to content

Commit 9a666cf

Browse files
committed
Add missing withdraw cooldown with initial 0 value
1 parent 0e647e2 commit 9a666cf

File tree

5 files changed

+78
-26
lines changed

5 files changed

+78
-26
lines changed

contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsNode.sol

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ contract RocketDAOProtocolSettingsNode is RocketDAOProtocolSettings, RocketDAOPr
2323
_setSettingUint("node.per.minipool.stake.maximum", 1.5 ether); // 150% of node ETH value (bonded ETH)
2424
_setSettingUint("reduced.bond", 4 ether); // 4 ETH (RPIP-42)
2525
_setSettingUint("node.unstaking.period", 28 days); // 28 days (RPIP-30)
26+
_setSettingUint("node.withdrawal.cooldown", 0); // No cooldown (RPIP-30)
2627
// Update deployed flag
2728
setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true);
2829
}
@@ -42,6 +43,8 @@ contract RocketDAOProtocolSettingsNode is RocketDAOProtocolSettings, RocketDAOPr
4243
require(_value >= 1 ether && _value <= 4 ether, "Value must be >= 1 ETH & <= 4 ETH");
4344
} else if(settingKey == keccak256(bytes("node.unstaking.period"))) {
4445
require(_value <= 6 weeks, "Value must be <= 6 weeks");
46+
} else if(settingKey == keccak256(bytes("node.withdrawal.cooldown"))) {
47+
require(_value <= 6 weeks, "Value must be <= 6 weeks");
4548
}
4649
}
4750
// Update setting now
@@ -103,8 +106,13 @@ contract RocketDAOProtocolSettingsNode is RocketDAOProtocolSettings, RocketDAOPr
103106
return amounts;
104107
}
105108

106-
/// @notice Returns the amount of time that must be waiting after unstaking RPL before it can be returned
109+
/// @notice Returns the amount of time that must be waited after unstaking RPL before it can be returned
107110
function getUnstakingPeriod() override external view returns (uint256) {
108111
return getSettingUint("node.unstaking.period");
109112
}
113+
114+
/// @notice Returns the amount of time that must be waited after staking RPL before it can be unstaked again
115+
function getWithdrawalCooldown() override external view returns (uint256) {
116+
return getSettingUint("node.withdrawal.cooldown");
117+
}
110118
}

contracts/contract/node/RocketNodeStaking.sol

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
// SPDX-License-Identifier: GPL-3.0-only
22
pragma solidity 0.8.30;
33

4-
import "../../interface/RocketVaultInterface.sol";
5-
6-
import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol";
7-
import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol";
8-
import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsRewardsInterface.sol";
9-
import "../../interface/minipool/RocketMinipoolManagerInterface.sol";
10-
import "../../interface/network/RocketNetworkPricesInterface.sol";
11-
import "../../interface/network/RocketNetworkSnapshotsInterface.sol";
12-
import "../../interface/network/RocketNetworkVotingInterface.sol";
13-
import "../../interface/node/RocketNodeManagerInterface.sol";
14-
import "../../interface/node/RocketNodeStakingInterface.sol";
15-
import "../../interface/token/RocketTokenRPLInterface.sol";
16-
import "../../interface/util/AddressSetStorageInterface.sol";
17-
import "../../interface/util/IERC20.sol";
18-
import "../RocketBase.sol";
19-
import "../network/RocketNetworkSnapshots.sol";
4+
import {RocketStorageInterface} from "../../interface/RocketStorageInterface.sol";
5+
import {RocketVaultInterface} from "../../interface/RocketVaultInterface.sol";
6+
import {RocketDAOProtocolSettingsNodeInterface} from "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol";
7+
import {RocketMinipoolManagerInterface} from "../../interface/minipool/RocketMinipoolManagerInterface.sol";
8+
import {RocketNetworkPricesInterface} from "../../interface/network/RocketNetworkPricesInterface.sol";
9+
import {RocketNetworkSnapshotsInterface} from "../../interface/network/RocketNetworkSnapshotsInterface.sol";
10+
import {RocketNodeManagerInterface} from "../../interface/node/RocketNodeManagerInterface.sol";
11+
import {RocketNodeStakingInterface} from "../../interface/node/RocketNodeStakingInterface.sol";
12+
import {RocketTokenRPLInterface} from "../../interface/token/RocketTokenRPLInterface.sol";
13+
import {IERC20} from "../../interface/util/IERC20.sol";
14+
import {IERC20Burnable} from "../../interface/util/IERC20Burnable.sol";
15+
import {RocketBase} from "../RocketBase.sol";
2016

2117
/// @notice Handles staking of RPL by node operators
2218
contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface {
@@ -192,8 +188,12 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface {
192188

193189
/// @dev Internal implementation for staking process
194190
function _stakeRPLFor(address _nodeAddress, uint256 _amount) internal {
191+
// Transfer RPL in and increase stake
195192
transferRPLIn(msg.sender, _amount);
196193
increaseNodeRPLStake(_nodeAddress, _amount);
194+
// Update last staked time
195+
setNodeLastStakeTime(_nodeAddress);
196+
// Emit event
197197
emit RPLStaked(_nodeAddress, msg.sender, _amount, block.timestamp);
198198
}
199199

@@ -254,6 +254,10 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface {
254254
if (timeSinceLastUnstake <= unstakingPeriod) {
255255
return 0;
256256
}
257+
// Check withdrawal cooldown
258+
if (block.timestamp - getNodeRPLStakedTime(_nodeAddress) < rocketDAOProtocolSettingsNode.getWithdrawalCooldown()) {
259+
return 0;
260+
}
257261
// Retrieve amount of RPL in unstaking state
258262
bytes32 unstakingKey = keccak256(abi.encodePacked("rpl.megapool.unstaking.amount", _nodeAddress));
259263
uint256 amountToWithdraw = getUint(unstakingKey);
@@ -366,9 +370,9 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface {
366370
uint256 rplSlashAmount = calcBase * _ethSlashAmount / rocketNetworkPrices.getRPLPrice();
367371
// Cap slashed amount to node's RPL stake
368372
uint256 rplStake = getNodeLegacyStakedRPL(_nodeAddress);
369-
if (rplSlashAmount > rplStake) { rplSlashAmount = rplStake; }
373+
if (rplSlashAmount > rplStake) {rplSlashAmount = rplStake;}
370374
// Transfer slashed amount to auction contract
371-
if(rplSlashAmount > 0) rocketVault.transferToken("rocketAuctionManager", IERC20(getContractAddress("rocketTokenRPL")), rplSlashAmount);
375+
if (rplSlashAmount > 0) rocketVault.transferToken("rocketAuctionManager", IERC20(getContractAddress("rocketTokenRPL")), rplSlashAmount);
372376
// Update RPL stake amounts
373377
decreaseNodeLegacyRPLStake(_nodeAddress, rplSlashAmount);
374378
// Mark minipool as slashed
@@ -426,7 +430,7 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface {
426430
// Check node operator has sufficient RPL to reduce
427431
uint256 legacyStakedRPL = getNodeLegacyStakedRPL(_nodeAddress);
428432
uint256 lockedRPL = getNodeLockedRPL(_nodeAddress);
429-
require (
433+
require(
430434
uint256(totalStakedRPL) >= _amount + lockedRPL &&
431435
uint256(totalStakedRPL) >= _amount + legacyStakedRPL,
432436
"Insufficient RPL stake to reduce"
@@ -450,13 +454,13 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface {
450454
uint256 legacyStakedRPL = getUint(legacyKey);
451455
// Check amount after decrease does not fall below minimum requirement for minipool bond
452456
uint256 maximumStakedRPL = getNodeMaximumRPLStakeForMinipools(_nodeAddress);
453-
require (
457+
require(
454458
legacyStakedRPL >= _amount + maximumStakedRPL,
455459
"Insufficient legacy staked RPL"
456460
);
457461
uint256 lockedRPL = getNodeLockedRPL(_nodeAddress);
458462
// Check node has enough unlocked RPL for the reduction
459-
require (
463+
require(
460464
uint256(totalStakedRPL) >= _amount + lockedRPL,
461465
"Insufficient RPL stake to reduce"
462466
);
@@ -546,7 +550,7 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface {
546550
/// @dev If legacy RPL balance has not been migrated, migrate it. Otherwise, do nothing
547551
function migrateLegacy(address _nodeAddress, uint256 _amount) private {
548552
bytes32 migratedKey = keccak256(abi.encodePacked("rpl.legacy.staked.node.migrated", _nodeAddress));
549-
if (getBool(migratedKey) ) {
553+
if (getBool(migratedKey)) {
550554
return;
551555
}
552556
bytes32 legacyKey = keccak256(abi.encodePacked("rpl.legacy.staked.node.amount", _nodeAddress));
@@ -575,6 +579,11 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface {
575579
setUint(keccak256(abi.encodePacked("rpl.megapool.unstake.time", _nodeAddress)), block.timestamp);
576580
}
577581

582+
/// @dev Sets the time of the given node operator's stake to the current block time
583+
function setNodeLastStakeTime(address _nodeAddress) internal {
584+
setUint(keccak256(abi.encodePacked("rpl.staked.node.time", _nodeAddress)), block.timestamp);
585+
}
586+
578587
/// @dev Implements caller restrictions (per RPIP-31):
579588
/// - If a node’s RPL withdrawal address is unset, the call MUST come from one of: the node’s primary withdrawal address, or the node’s address
580589
/// - If a node’s RPL withdrawal address is set, the call MUST come from the current RPL withdrawal address

contracts/contract/upgrade/RocketUpgradeOneDotFour.sol

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,9 @@ contract RocketUpgradeOneDotFour is RocketBase {
120120
{
121121
bytes32 settingNameSpace = keccak256(abi.encodePacked("dao.protocol.setting.", "node"));
122122
// Initialised reduced_bond and unstaking_period setting per RPIP-42 and RPIP-30
123-
setUint(keccak256(abi.encodePacked(settingNameSpace, "reduced.bond")), 4 ether); // 4 ether (RPIP-42)
124-
setUint(keccak256(abi.encodePacked(settingNameSpace, "node.unstaking.period")), 28 days); // 28 days (RPIP-30)
123+
setUint(keccak256(abi.encodePacked(settingNameSpace, "reduced.bond")), 4 ether); // 4 ether (RPIP-42)
124+
setUint(keccak256(abi.encodePacked(settingNameSpace, "node.unstaking.period")), 28 days); // 28 days (RPIP-30)
125+
setUint(keccak256(abi.encodePacked(settingNameSpace, "node.withdrawal.cooldown")), 0); // No cooldown (RPIP-30)
125126
}
126127

127128
// Minipool settings

contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ interface RocketDAOProtocolSettingsNodeInterface {
1313
function getReducedBond() external view returns (uint256);
1414
function getBaseBondArray() external view returns (uint256[] memory);
1515
function getUnstakingPeriod() external view returns (uint256);
16+
function getWithdrawalCooldown() external view returns (uint256);
1617
}

test/node/node-staking-tests.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { before, describe, it } from 'mocha';
2-
import { RocketDAONodeTrustedSettingsMinipool, RocketNodeStaking, StakeHelper } from '../_utils/artifacts';
2+
import {
3+
RocketDAONodeTrustedSettingsMinipool,
4+
RocketDAOProtocolSettingsNode,
5+
RocketNodeStaking,
6+
StakeHelper,
7+
} from '../_utils/artifacts';
38
import { printTitle } from '../_utils/formatting';
49
import { shouldRevert } from '../_utils/testing';
510
import {
@@ -21,6 +26,7 @@ import { globalSnapShot, snapshotDescribe } from '../_utils/snapshotting';
2126
import { unstakeRpl, unstakeRplFor } from './scenario-unstake-rpl';
2227
import { assertBN } from '../_helpers/bn';
2328
import { unstakeLegacyRpl, unstakeLegacyRplFor } from './scenario-unstake-legacy-rpl';
29+
import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap';
2430

2531
const helpers = require('@nomicfoundation/hardhat-network-helpers');
2632
const hre = require('hardhat');
@@ -201,6 +207,33 @@ export default function() {
201207
await assertUnstakingBalance(node, '1000'.ether);
202208
});
203209

210+
it(printTitle('node operator', 'can not withdraw RPL if unstaking period has passed, but withdrawal cooldown has not'), async () => {
211+
// Set cooldown to 5 days
212+
const withdrawalCooldown = 60 * 60 * 24 * 5;
213+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, 'node.withdrawal.cooldown', withdrawalCooldown, { from: owner });
214+
// Stake 1,000 megapool RPL
215+
await nodeStakeRPL('1000'.ether, { from: node });
216+
// Unstake 1,000 RPL
217+
await unstakeRpl('1000'.ether, { from: node });
218+
// Assert balances
219+
await assertBalances(node, 0n, '0'.ether);
220+
await assertUnstakingBalance(node, '1000'.ether);
221+
// Wait 28 days (unstaking period)
222+
await helpers.time.increase(60 * 60 * 24 * 28 + 1);
223+
// Stake another 1 RPL to reset cooldown
224+
await nodeStakeRPL('1'.ether, { from: node });
225+
// Should be unable to withdraw due to cooldown
226+
await shouldRevert(
227+
withdrawRpl({ from: node }),
228+
'Was able to withdraw before cooldown',
229+
'No available unstaking RPL to withdraw'
230+
);
231+
// Wait 5 days
232+
await helpers.time.increase(withdrawalCooldown + 1);
233+
// Should be able to withdraw now
234+
await withdrawRpl({ from: node });
235+
});
236+
204237
it(printTitle('node operator', 'can unstake RPL from RPL withdrawal address'), async () => {
205238
// Stake 10,000 megapool RPL
206239
const rplAmount = '10000'.ether;

0 commit comments

Comments
 (0)