Skip to content

Commit 5a374d9

Browse files
committed
Correct legacy unstaking to use borrowed instead of bonded
1 parent 85cdf8d commit 5a374d9

File tree

9 files changed

+102
-40
lines changed

9 files changed

+102
-40
lines changed

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

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ contract RocketDAOProtocolSettingsNode is RocketDAOProtocolSettings, RocketDAOPr
1919
setSettingBool("node.smoothing.pool.registration.enabled", true);
2020
setSettingBool("node.deposit.enabled", false);
2121
setSettingBool("node.vacant.minipools.enabled", false);
22-
_setSettingUint("node.per.minipool.stake.minimum", 0.1 ether); // 10% of user ETH value (borrowed ETH)
23-
_setSettingUint("node.per.minipool.stake.maximum", 1.5 ether); // 150% of node ETH value (bonded ETH)
2422
_setSettingUint("reduced.bond", 4 ether); // 4 ETH (RPIP-42)
2523
_setSettingUint("node.unstaking.period", 28 days); // 28 days (RPIP-30)
2624
_setSettingUint("node.withdrawal.cooldown", 0); // No cooldown (RPIP-30)
25+
_setSettingUint("node.minimum.legacy.staked.rpl", 0.15 ether); // 15% of borrowed ETH (RPIP-30)
2726
// Update deployed flag
2827
setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true);
2928
}
@@ -76,16 +75,6 @@ contract RocketDAOProtocolSettingsNode is RocketDAOProtocolSettings, RocketDAOPr
7675
return getSettingBool("node.vacant.minipools.enabled");
7776
}
7877

79-
// Minimum RPL stake per minipool as a fraction of assigned user ETH value
80-
function getMinimumPerMinipoolStake() override external view returns (uint256) {
81-
return getSettingUint("node.per.minipool.stake.minimum");
82-
}
83-
84-
// Maximum RPL stake per minipool as a fraction of assigned user ETH value
85-
function getMaximumPerMinipoolStake() override external view returns (uint256) {
86-
return getSettingUint("node.per.minipool.stake.maximum");
87-
}
88-
8978
// Maximum staked RPL that applies to voting power per minipool as a fraction of assigned user ETH value
9079
function getMaximumStakeForVotingPower() override external view returns (uint256) {
9180
bytes32 settingKey = keccak256(bytes("node.voting.power.stake.maximum"));
@@ -115,4 +104,9 @@ contract RocketDAOProtocolSettingsNode is RocketDAOProtocolSettings, RocketDAOPr
115104
function getWithdrawalCooldown() override external view returns (uint256) {
116105
return getSettingUint("node.withdrawal.cooldown");
117106
}
107+
108+
/// @notice Returns the amount of legacy staked RPL required by a node after unstaking as percentage of their borrowed ETH
109+
function getMinimumLegacyRPLStake() override external view returns (uint256) {
110+
return getSettingUint("node.minimum.legacy.staked.rpl");
111+
}
118112
}

contracts/contract/node/RocketNodeStaking.sol

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface {
421421
}
422422

423423
/// @dev Decreases a node operator's megapool staked RPL amount
424+
/// @param _nodeAddress Address of node to decrease megapool staked RPL for
424425
/// @param _amount Amount to decrease by
425426
function decreaseNodeMegapoolRPLStake(address _nodeAddress, uint256 _amount) private {
426427
RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots"));
@@ -443,6 +444,7 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface {
443444
}
444445

445446
/// @dev Decreases a node operator's legacy staked RPL amount
447+
/// @param _nodeAddress Address of node to decrease legacy staked RPL for
446448
/// @param _amount Amount to decrease by
447449
function decreaseNodeLegacyRPLStake(address _nodeAddress, uint256 _amount) private {
448450
RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots"));
@@ -452,10 +454,10 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface {
452454
// Check amount does not exceed amount staked
453455
bytes32 legacyKey = keccak256(abi.encodePacked("rpl.legacy.staked.node.amount", _nodeAddress));
454456
uint256 legacyStakedRPL = getUint(legacyKey);
455-
// Check amount after decrease does not fall below minimum requirement for minipool bond
456-
uint256 maximumStakedRPL = getNodeMaximumRPLStakeForMinipools(_nodeAddress);
457+
// Check amount after decrease does not fall below minimum required
458+
uint256 minimumLegacyStakedRPL = getNodeMinimumLegacyRPLStake(_nodeAddress);
457459
require(
458-
legacyStakedRPL >= _amount + maximumStakedRPL,
460+
legacyStakedRPL >= _amount + minimumLegacyStakedRPL,
459461
"Insufficient legacy staked RPL"
460462
);
461463
uint256 lockedRPL = getNodeLockedRPL(_nodeAddress);
@@ -535,16 +537,16 @@ contract RocketNodeStaking is RocketBase, RocketNodeStakingInterface {
535537
return (ethTotal * calcBase) / (ethTotal - borrowedETH);
536538
}
537539

538-
/// @notice Returns a node's maximum RPL stake to fully collateralise their minipools
540+
/// @notice Returns the minimum amount of legacy staked RPL a node must have after unstaking
539541
/// @param _nodeAddress The address of the node operator to calculate for
540-
function getNodeMaximumRPLStakeForMinipools(address _nodeAddress) public view returns (uint256) {
542+
function getNodeMinimumLegacyRPLStake(address _nodeAddress) public view returns (uint256) {
541543
// Load contracts
542544
RocketNetworkPricesInterface rocketNetworkPrices = RocketNetworkPricesInterface(getContractAddress("rocketNetworkPrices"));
543545
RocketDAOProtocolSettingsNodeInterface rocketDAOProtocolSettingsNode = RocketDAOProtocolSettingsNodeInterface(getContractAddress("rocketDAOProtocolSettingsNode"));
544-
// Retrieve variables
545-
uint256 maximumStakePercent = rocketDAOProtocolSettingsNode.getMaximumPerMinipoolStake();
546-
uint256 bondedETH = getNodeMinipoolETHBonded(_nodeAddress);
547-
return bondedETH * maximumStakePercent / rocketNetworkPrices.getRPLPrice();
546+
// Calculate and return minimum
547+
uint256 minimumRPLStakePercent = rocketDAOProtocolSettingsNode.getMinimumLegacyRPLStake();
548+
uint256 borrowedETH = getNodeMinipoolETHBorrowed(_nodeAddress);
549+
return borrowedETH * minimumRPLStakePercent / rocketNetworkPrices.getRPLPrice();
548550
}
549551

550552
/// @dev If legacy RPL balance has not been migrated, migrate it. Otherwise, do nothing

contracts/contract/upgrade/RocketUpgradeOneDotFour.sol

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@ 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)
125-
setUint(keccak256(abi.encodePacked(settingNameSpace, "node.withdrawal.cooldown")), 0); // No cooldown (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)
126+
setUint(keccak256(abi.encodePacked(settingNameSpace, "node.minimum.legacy.staked.rpl")), 0.15 ether); // 15% (RPIP-30)
126127
}
127128

128129
// Minipool settings

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ interface RocketDAOProtocolSettingsNodeInterface {
77
function getSmoothingPoolRegistrationEnabled() external view returns (bool);
88
function getDepositEnabled() external view returns (bool);
99
function getVacantMinipoolsEnabled() external view returns (bool);
10-
function getMinimumPerMinipoolStake() external view returns (uint256);
11-
function getMaximumPerMinipoolStake() external view returns (uint256);
1210
function getMaximumStakeForVotingPower() external view returns (uint256);
1311
function getReducedBond() external view returns (uint256);
1412
function getBaseBondArray() external view returns (uint256[] memory);
1513
function getUnstakingPeriod() external view returns (uint256);
1614
function getWithdrawalCooldown() external view returns (uint256);
15+
function getMinimumLegacyRPLStake() external view returns (uint256);
1716
}

contracts/interface/node/RocketNodeStakingInterface.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ interface RocketNodeStakingInterface {
3939
function getNodeMegapoolETHBorrowed(address _nodeAddress) external view returns (uint256);
4040
function getNodeMinipoolETHBorrowed(address _nodeAddress) external view returns (uint256);
4141

42-
function getNodeMaximumRPLStakeForMinipools(address _nodeAddress) external view returns (uint256);
42+
function getNodeMinimumLegacyRPLStake(address _nodeAddress) external view returns (uint256);
4343
function getNodeETHCollateralisationRatio(address _nodeAddress) external view returns (uint256);
4444

4545
// Internal (not callable by users)

test-upgrade/tests/staking-tests.js

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { executeUpgrade } from '../_helpers/upgrade';
2-
import { RocketNetworkVoting, RocketNodeStaking } from '../../test/_utils/artifacts';
2+
import { RocketDAOProtocolSettingsNode, RocketNetworkVoting, RocketNodeStaking } from '../../test/_utils/artifacts';
33
import { assertBN } from '../../test/_helpers/bn';
4-
import { getMinipoolMinimumRPLStake, stakeMinipool } from '../../test/_helpers/minipool';
4+
import { stakeMinipool } from '../../test/_helpers/minipool';
55
import { createMinipool } from '../_helpers/minipool';
66
import { userDeposit } from '../../test/_helpers/deposit';
77
import { nodeDeposit } from '../../test/_helpers/megapool';
88
import { BigSqrt } from '../../test/_helpers/bigmath';
9+
import { submitPrices } from '../../test/network/scenario-submit-prices';
10+
import { setNodeTrusted } from '../../test/_helpers/node';
11+
import { shouldRevert } from '../../test/_utils/testing';
12+
import { setDAOProtocolBootstrapSetting } from '../../test/dao/scenario-dao-protocol-bootstrap';
913

1014
const { beforeEach, describe, before, it } = require('mocha');
1115
const { globalSnapShot } = require('../../test/_utils/snapshotting');
@@ -30,6 +34,9 @@ export default function() {
3034
let owner,
3135
node,
3236
nodeWithdrawalAddress,
37+
trustedNode1,
38+
trustedNode2,
39+
trustedNode3,
3340
random;
3441

3542
let upgradeContract;
@@ -41,6 +48,9 @@ export default function() {
4148
owner,
4249
node,
4350
nodeWithdrawalAddress,
51+
trustedNode1,
52+
trustedNode2,
53+
trustedNode3,
4454
random,
4555
] = await ethers.getSigners();
4656

@@ -134,5 +144,68 @@ export default function() {
134144
*/
135145
assertBN.equal(votingPower, BigSqrt('3000'.ether * '1'.ether));
136146
});
147+
148+
it(printTitle('node', 'can unstake legacy staked RPL down to 15% of borrowed ETH'), async () => {
149+
// Register node
150+
await registerNode({ from: node });
151+
// Register trusted nodes
152+
await registerNode({ from: trustedNode1 });
153+
await registerNode({ from: trustedNode2 });
154+
await registerNode({ from: trustedNode3 });
155+
await setNodeTrusted(trustedNode1, 'saas_1', 'node@home.com', owner);
156+
await setNodeTrusted(trustedNode2, 'saas_2', 'node@home.com', owner);
157+
await setNodeTrusted(trustedNode3, 'saas_3', 'node@home.com', owner);
158+
// Set RPL price to 0.1 ETH
159+
let block = await ethers.provider.getBlockNumber();
160+
let slotTimestamp = '1600000000';
161+
let rplPrice = '0.1'.ether;
162+
// Submit different prices
163+
await submitPrices(block, slotTimestamp, rplPrice, {
164+
from: trustedNode1,
165+
});
166+
await submitPrices(block, slotTimestamp, rplPrice, {
167+
from: trustedNode2,
168+
});
169+
await submitPrices(block, slotTimestamp, rplPrice, {
170+
from: trustedNode3,
171+
});
172+
// Mint 1000 RPL and stake
173+
await mintRPL(owner, node, '1000'.ether);
174+
await stakeRPL(node, '1000'.ether);
175+
// Create a 8 ETH minipool
176+
const minipool = (await createMinipool({ from: node, value: '8'.ether })).connect(node);
177+
// Perform a user deposit with enough to assign the minipool
178+
await userDeposit({ from: random, value: '24'.ether });
179+
// Confirm prelaunch status
180+
const status = await minipool.getStatus();
181+
assertBN.equal(status, 1n)
182+
// Execute upgrade
183+
await executeUpgrade(owner, upgradeContract, rocketStorageAddress);
184+
// Set minimum stake setting to 15%
185+
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNode, "node.minimum.legacy.staked.rpl", '0.15'.ether, { from: owner });
186+
// Check minimum stake
187+
/**
188+
* With 1x 8 ETH minipool, borrowed ETH is 24 ETH
189+
* At 0.1 ETH per RPL, the minimum should be 15% of 240 RPL
190+
* Minimum is therefore 36 RPL
191+
*/
192+
const rocketNodeStaking = await RocketNodeStaking.deployed();
193+
const minimumStake = await rocketNodeStaking.getNodeMinimumLegacyRPLStake(node.address);
194+
assertBN.equal(minimumStake, '36'.ether);
195+
// Should not be able to unstake below 36 RPL (1000 - 36 = 964)
196+
await shouldRevert(
197+
unstakeLegacyRpl('965'.ether, { from: node }),
198+
'Was able to unstake below 15% minimum',
199+
'Insufficient legacy staked RPL'
200+
);
201+
// Should be able to unstake to 36 RPL
202+
await unstakeLegacyRpl('964'.ether, { from: node });
203+
// Should not be able to unstake any more
204+
await shouldRevert(
205+
unstakeLegacyRpl(1n, { from: node }),
206+
'Was able to unstake below 15% minimum',
207+
'Insufficient legacy staked RPL'
208+
);
209+
});
137210
});
138211
}

test/dao/dao-protocol-tests.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,8 @@ export default function() {
291291
});
292292

293293
async function createNode(validatorCount, node) {
294-
// Stake RPL to cover minipools
295-
let minipoolRplStake = await getMinipoolMinimumRPLStake();
296-
let rplStake = minipoolRplStake * validatorCount.BN;
294+
// Stake RPL for voting power
295+
let rplStake = '100'.ether * validatorCount.BN;
297296
const nodeCount = await getNodeCount();
298297
await registerNode({ from: node });
299298
nodeMap[node.address] = Number(nodeCount);

test/deposit/deposit-pool-tests.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,6 @@ export default function() {
141141
});
142142
// Disable deposit assignment
143143
await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', false, { from: owner });
144-
// Stake RPL to cover minipools
145-
let minipoolRplStake = await getMinipoolMinimumRPLStake();
146-
let rplStake = minipoolRplStake * 3n;
147-
await mintRPL(owner, trustedNode, rplStake);
148-
await nodeStakeRPL(rplStake, { from: trustedNode });
149144
// Deposit and queue up some validators
150145
await userDeposit({ from: staker, value: '100'.ether });
151146
for (let i = 0; i < 3; ++i) {

test/network/network-voting-tests.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,8 @@ export default function() {
4040
// Register node & set withdrawal address
4141
await registerNode({ from: node });
4242

43-
// Stake RPL to cover minipools
44-
let minipoolRplStake = await getMinipoolMaximumRPLStake();
45-
let rplStake = minipoolRplStake * 2n;
43+
// Stake RPL for voting power
44+
let rplStake = '1200'.ether;
4645
await mintRPL(owner, node, rplStake);
4746
await nodeStakeRPL(rplStake, { from: node });
4847

0 commit comments

Comments
 (0)