diff --git a/contracts/contract/minipool/RocketMinipoolDelegate.sol b/contracts/contract/minipool/RocketMinipoolDelegate.sol index 7e2a19058..7c332903f 100644 --- a/contracts/contract/minipool/RocketMinipoolDelegate.sol +++ b/contracts/contract/minipool/RocketMinipoolDelegate.sol @@ -546,6 +546,10 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); require(rocketMinipoolManager.getMinipoolExists(address(this)), "Minipool already closed"); rocketMinipoolManager.destroyMinipool(); + // Clear state + userDepositBalance = 0; + userDepositBalanceLegacy = 0; + userDepositAssignedTime = 0; } /// @notice Can be called by trusted nodes to scrub this minipool if its withdrawal credentials are not set correctly @@ -637,20 +641,23 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn require(_amount < previousBond, "Bond must be lower than current amount"); require(status == MinipoolStatus.Staking, "Minipool must be staking"); // Get contracts - RocketNodeDepositInterface rocketNodeDepositInterface = RocketNodeDepositInterface(getContractAddress("rocketNodeDeposit")); + RocketNodeDepositInterface rocketNodeDeposit = RocketNodeDepositInterface(getContractAddress("rocketNodeDeposit")); // Check the new bond amount is valid - require(rocketNodeDepositInterface.isValidDepositAmount(_amount), "Invalid bond amount"); + require(rocketNodeDeposit.isValidDepositAmount(_amount), "Invalid bond amount"); // Distribute any skimmed rewards distributeSkimmedRewards(); - // Update user/node balances + // Calculate bond difference uint256 delta = previousBond.sub(_amount); + // Increase ETH matched or revert if exceeds limit based on current RPL stake + rocketNodeDeposit.increaseEthMatched(nodeAddress, delta); + // Update user/node balances userDepositBalance = getUserDepositBalance().add(delta); nodeDepositBalance = _amount; // Reset node fee to current network rate RocketNetworkFeesInterface rocketNetworkFees = RocketNetworkFeesInterface(getContractAddress("rocketNetworkFees")); nodeFee = rocketNetworkFees.getNodeFee(); // Increase node operator's deposit credit - rocketNodeDepositInterface.increaseDepositCreditBalance(nodeAddress, delta); + rocketNodeDeposit.increaseDepositCreditBalance(nodeAddress, delta); // Break state to prevent rollback exploit if (depositType != MinipoolDeposit.Variable) { userDepositBalanceLegacy = 2**256-1; @@ -724,9 +731,6 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn emit EtherWithdrawn(address(rocketDepositPool), userCapital, block.timestamp); } // Clear storage - userDepositBalance = 0; - userDepositBalanceLegacy = 0; - userDepositAssignedTime = 0; nodeDepositBalance = 0; nodeRefundBalance = 0; } diff --git a/contracts/contract/minipool/RocketMinipoolFactory.sol b/contracts/contract/minipool/RocketMinipoolFactory.sol index ad14d0ef6..0b9e11482 100644 --- a/contracts/contract/minipool/RocketMinipoolFactory.sol +++ b/contracts/contract/minipool/RocketMinipoolFactory.sol @@ -41,7 +41,7 @@ contract RocketMinipoolFactory is RocketBase, RocketMinipoolFactoryInterface { function deployContract(address _nodeAddress, uint256 _salt) override external onlyLatestContract("rocketMinipoolFactory", address(this)) onlyLatestContract("rocketMinipoolManager", msg.sender) returns (address) { // Construct deployment bytecode bytes memory creationCode = getMinipoolBytecode(); - bytes memory bytecode = abi.encodePacked(creationCode, abi.encode(rocketStorage, _nodeAddress)); + bytes memory bytecode = abi.encodePacked(creationCode, abi.encode(rocketStorage)); // Construct final salt uint256 salt = uint256(keccak256(abi.encodePacked(_nodeAddress, _salt))); // CREATE2 deployment diff --git a/contracts/contract/node/RocketNodeDeposit.sol b/contracts/contract/node/RocketNodeDeposit.sol index 67c5beb14..580c4f1e9 100644 --- a/contracts/contract/node/RocketNodeDeposit.sol +++ b/contracts/contract/node/RocketNodeDeposit.sol @@ -99,7 +99,7 @@ contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); launchAmount = rocketDAOProtocolSettingsMinipool.getLaunchBalance(); } - increaseEthMatched(msg.sender, launchAmount.sub(_bondAmount)); + _increaseEthMatched(msg.sender, launchAmount.sub(_bondAmount)); // Create the minipool RocketMinipoolInterface minipool = createMinipool(_salt, _expectedMinipoolAddress); // Get the pre-launch value @@ -140,14 +140,22 @@ contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { // Increase ETH matched (used to calculate RPL collateral requirements) RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); uint256 launchAmount = rocketDAOProtocolSettingsMinipool.getLaunchBalance(); - increaseEthMatched(msg.sender, launchAmount.sub(_bondAmount)); + _increaseEthMatched(msg.sender, launchAmount.sub(_bondAmount)); // Create the minipool _createVacantMinipool(_salt, _validatorPubkey, _bondAmount, _expectedMinipoolAddress); } + /// @notice Called by minipools during bond reduction to increase the amount of ETH the node operator has + /// @param _nodeAddress The node operator's address to increase the ETH matched for + /// @param _amount The amount to increase the ETH matched + /// @dev Will revert if the new ETH matched amount exceeds the node operators limit + function increaseEthMatched(address _nodeAddress, uint256 _amount) override external onlyLatestContract("rocketNodeDeposit", address(this)) onlyRegisteredMinipool(msg.sender) { + _increaseEthMatched(_nodeAddress, _amount); + } + /// @dev Increases the amount of ETH that has been matched against a node operators bond. Reverts if it exceeds the /// collateralisation requirements of the network - function increaseEthMatched(address _nodeAddress, uint256 _amount) private { + function _increaseEthMatched(address _nodeAddress, uint256 _amount) private { // Check amount doesn't exceed limits RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress("rocketNodeStaking")); require( diff --git a/contracts/contract/upgrade/RocketUpgradeOneDotTwo.sol b/contracts/contract/upgrade/RocketUpgradeOneDotTwo.sol index 7c1cb8762..ada186a24 100644 --- a/contracts/contract/upgrade/RocketUpgradeOneDotTwo.sol +++ b/contracts/contract/upgrade/RocketUpgradeOneDotTwo.sol @@ -58,6 +58,8 @@ contract RocketUpgradeOneDotTwo is RocketBase { string public newRocketNodeManagerAbi; string public rocketMinipoolBaseAbi; + string public newRocketMinipoolAbi; + // Save deployer to limit access to set functions address immutable deployer; @@ -115,6 +117,7 @@ contract RocketUpgradeOneDotTwo is RocketBase { newRocketDAONodeTrustedSettingsMinipoolAbi = _abis[12]; newRocketNodeManagerAbi = _abis[13]; rocketMinipoolBaseAbi = _abis[14]; + newRocketMinipoolAbi = _abis[15]; } function setInterval(uint256 _interval, uint256 _block) external { @@ -156,6 +159,9 @@ contract RocketUpgradeOneDotTwo is RocketBase { // Add new contracts _addContract("rocketMinipoolBase", rocketMinipoolBase, rocketMinipoolBaseAbi); + // Upgrade ABIs + _upgradeABI("rocketMinipool", newRocketMinipoolAbi); + // Migrate settings bytes32 settingNameSpace = keccak256(abi.encodePacked("dao.protocol.setting.", "deposit")); setUint(keccak256(abi.encodePacked(settingNameSpace, "deposit.assign.maximum")), 90); @@ -241,4 +247,15 @@ contract RocketUpgradeOneDotTwo is RocketBase { deleteString(keccak256(abi.encodePacked("contract.abi", _name))); } + /// @dev Upgrade a network contract ABI + function _upgradeABI(string memory _name, string memory _contractAbi) internal { + // Check ABI exists + string memory existingAbi = getString(keccak256(abi.encodePacked("contract.abi", _name))); + require(bytes(existingAbi).length > 0, "ABI does not exist"); + // Sanity checks + require(bytes(_contractAbi).length > 0, "Empty ABI is invalid"); + require(keccak256(bytes(existingAbi)) != keccak256(bytes(_contractAbi)), "ABIs are identical"); + // Set ABI + setString(keccak256(abi.encodePacked("contract.abi", _name)), _contractAbi); + } } \ No newline at end of file diff --git a/contracts/interface/node/RocketNodeDepositInterface.sol b/contracts/interface/node/RocketNodeDepositInterface.sol index 2be10229b..5bf658639 100644 --- a/contracts/interface/node/RocketNodeDepositInterface.sol +++ b/contracts/interface/node/RocketNodeDepositInterface.sol @@ -11,4 +11,5 @@ interface RocketNodeDepositInterface { function isValidDepositAmount(uint256 _amount) external pure returns (bool); function getDepositAmounts() external pure returns (uint256[] memory); function createVacantMinipool(uint256 _bondAmount, uint256 _minimumNodeFee, bytes calldata _validatorPubkey, uint256 _salt, address _expectedMinipoolAddress) external; + function increaseEthMatched(address _nodeAddress, uint256 _amount) external; } diff --git a/scripts/deploy-upgrade-v1.2.js b/scripts/deploy-upgrade-v1.2.js index afe9e26a1..70d6cdc7c 100644 --- a/scripts/deploy-upgrade-v1.2.js +++ b/scripts/deploy-upgrade-v1.2.js @@ -38,6 +38,16 @@ const contracts = { rocketUpgradeOneDotTwo: artifacts.require('RocketUpgradeOneDotTwo.sol'), }; +// Construct ABI for rocketMinipool +const rocketMinipoolAbi = [] + .concat(artifacts.require('RocketMinipoolDelegate.sol').abi) + .concat(artifacts.require('RocketMinipoolBase.sol').abi) + .concat(artifacts.require('RocketMinipoolProxy.sol').abi) + .filter(i => i.type !== 'fallback' && i.type !== 'receive'); + +rocketMinipoolAbi.push({ stateMutability: 'payable', type: 'fallback'}); +rocketMinipoolAbi.push({ stateMutability: 'payable', type: 'receive'}); + /*** Deployment **********************/ // Upgrade Rocket Pool @@ -117,6 +127,7 @@ export async function upgrade() { compressABI(contracts.rocketDAONodeTrustedSettingsMinipool.abi), compressABI(contracts.rocketNodeManager.abi), compressABI(contracts.rocketMinipoolBase.abi), + rocketMinipoolAbi ], ]; await instance.set(...args); diff --git a/test/_helpers/deployment.js b/test/_helpers/deployment.js index a187bb788..abdef232f 100644 --- a/test/_helpers/deployment.js +++ b/test/_helpers/deployment.js @@ -116,6 +116,15 @@ const abis = { rocketMinipool: [artifacts.require('RocketMinipoolDelegateOld.sol'), artifacts.require('RocketMinipoolOld.sol')], }; +// Construct ABI for rocketMinipool +const rocketMinipoolAbi = [] + .concat(artifacts.require('RocketMinipoolDelegate.sol').abi) + .concat(artifacts.require('RocketMinipoolBase.sol').abi) + .concat(artifacts.require('RocketMinipoolProxy.sol').abi) + .filter(i => i.type !== 'fallback' && i.type !== 'receive'); + +rocketMinipoolAbi.push({ stateMutability: 'payable', type: 'fallback'}); +rocketMinipoolAbi.push({ stateMutability: 'payable', type: 'receive'}); /*** Deployment **********************/ @@ -266,6 +275,7 @@ export async function deployRocketPool() { compressABI(contracts.rocketDAONodeTrustedSettingsMinipoolNew.abi), compressABI(contracts.rocketNodeManagerNew.abi), compressABI(contracts.rocketMinipoolBase.abi), + compressABI(rocketMinipoolAbi), ], ] await upgrader.set(...args) diff --git a/test/_helpers/minipool.js b/test/_helpers/minipool.js index bd6fbad57..187e147e4 100644 --- a/test/_helpers/minipool.js +++ b/test/_helpers/minipool.js @@ -71,6 +71,10 @@ let minipoolSalt = 1 // Create a minipool export async function createMinipool(txOptions, salt = null) { + return createMinipoolWithBondAmount(txOptions.value, txOptions, salt); +} + +export async function createMinipoolWithBondAmount(bond, txOptions, salt = null) { const preUpdate = !(await upgradeExecuted()); // Load contracts @@ -101,7 +105,7 @@ export async function createMinipool(txOptions, salt = null) { const depositType = await rocketNodeDeposit.getDepositType(txOptions.value); constructorArgs = web3.eth.abi.encodeParameters(['address', 'address', 'uint8'], [rocketStorage.address, txOptions.from, depositType]); } else { - constructorArgs = web3.eth.abi.encodeParameters(['address', 'address'], [rocketStorage.address, txOptions.from]); + constructorArgs = web3.eth.abi.encodeParameters(['address'], [rocketStorage.address]); } const deployCode = contractBytecode + constructorArgs.substr(2); @@ -155,7 +159,7 @@ export async function createMinipool(txOptions, salt = null) { let depositDataRoot = getDepositDataRoot(depositData); - await rocketNodeDeposit.deposit(txOptions.value, '0'.ether, depositData.pubkey, depositData.signature, depositDataRoot, salt, '0x' + minipoolAddress, txOptions); + await rocketNodeDeposit.deposit(bond, '0'.ether, depositData.pubkey, depositData.signature, depositDataRoot, salt, '0x' + minipoolAddress, txOptions); } return RocketMinipoolDelegate.at('0x' + minipoolAddress); @@ -180,7 +184,7 @@ export async function createVancantMinipool(bondAmount, txOptions, salt = null) const contractBytecode = RocketMinipoolProxy.bytecode; // Construct creation code for minipool deploy - const constructorArgs = web3.eth.abi.encodeParameters(['address', 'address'], [rocketStorage.address, txOptions.from]); + const constructorArgs = web3.eth.abi.encodeParameters(['address'], [rocketStorage.address]); const deployCode = contractBytecode + constructorArgs.substr(2); if (salt === null){ diff --git a/test/_helpers/node.js b/test/_helpers/node.js index 44afde4b1..1a21b63e8 100644 --- a/test/_helpers/node.js +++ b/test/_helpers/node.js @@ -157,7 +157,7 @@ export async function nodeDeposit(txOptions) { const contractBytecode = RocketMinipoolProxy.bytecode; // Construct creation code for minipool deploy - const constructorArgs = web3.eth.abi.encodeParameters(['address', 'address'], [rocketStorage.address, txOptions.from]); + const constructorArgs = web3.eth.abi.encodeParameters(['address'], [rocketStorage.address]); const deployCode = contractBytecode + constructorArgs.substr(2); const salt = minipoolSalt++; diff --git a/test/minipool/scenario-dissolve.js b/test/minipool/scenario-dissolve.js index abeb1f947..534f8ed6a 100644 --- a/test/minipool/scenario-dissolve.js +++ b/test/minipool/scenario-dissolve.js @@ -26,5 +26,4 @@ export async function dissolve(minipool, txOptions) { // Check minipool details assertBN.notEqual(details1.status, minipoolStates.Dissolved, 'Incorrect initial minipool status'); assertBN.equal(details2.status, minipoolStates.Dissolved, 'Incorrect updated minipool status'); - assertBN.equal(details2.userDepositBalance, 0, 'Incorrect updated minipool user deposit balance'); } diff --git a/test/minipool/scenario-scrub.js b/test/minipool/scenario-scrub.js index 966612673..1a856c98c 100644 --- a/test/minipool/scenario-scrub.js +++ b/test/minipool/scenario-scrub.js @@ -55,7 +55,6 @@ export async function voteScrub(minipool, txOptions) { // Check state if (details1.votes.add('1'.BN).gt(quorum)){ assertBN.equal(details2.status, minipoolStates.Dissolved, 'Incorrect updated minipool status'); - assertBN.equal(details2.userDepositBalance, '0', 'Incorrect updated minipool user deposit balance'); // Check slashing if penalties are enabled if (details1.penaltyEnabled && !details1.vacant) { // Calculate amount slashed diff --git a/test/node/scenario-deposit-v2.js b/test/node/scenario-deposit-v2.js index a4ecf9e81..8f55fabfd 100644 --- a/test/node/scenario-deposit-v2.js +++ b/test/node/scenario-deposit-v2.js @@ -59,7 +59,7 @@ export async function depositV2(minimumNodeFee, bondAmount, txOptions) { const contractBytecode = RocketMinipoolProxy.bytecode; // Construct creation code for minipool deploy - const constructorArgs = web3.eth.abi.encodeParameters(['address', 'address'], [rocketStorage.address, txOptions.from]); + const constructorArgs = web3.eth.abi.encodeParameters(['address'], [rocketStorage.address]); const deployCode = contractBytecode + constructorArgs.substr(2); const salt = minipoolSalt++; diff --git a/test/upgrade/upgrade-tests.js b/test/upgrade/upgrade-tests.js index be5b7112f..96b8704cd 100644 --- a/test/upgrade/upgrade-tests.js +++ b/test/upgrade/upgrade-tests.js @@ -2,19 +2,28 @@ import { nodeStakeRPL, registerNode, setNodeTrusted } from '../_helpers/node'; import { upgradeOneDotTwo } from '../_utils/upgrade'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { setDAONodeTrustedBootstrapSetting } from '../dao/scenario-dao-node-trusted-bootstrap'; -import { createMinipool, minipoolStates, stakeMinipool } from '../_helpers/minipool'; +import { createMinipool, createMinipoolWithBondAmount, minipoolStates, stakeMinipool } from '../_helpers/minipool'; import { mintRPL } from '../_helpers/tokens'; import { userDeposit } from '../_helpers/deposit'; import { RocketDAONodeTrustedSettingsMinipool, + RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsMinipool, - RocketDepositPool, RocketMinipoolBase, RocketMinipoolQueue, RocketMinipoolQueueOld, RocketTokenRETH, + RocketDAOProtocolSettingsNetwork, + RocketDepositPool, + RocketMinipoolBase, + RocketMinipoolQueue, + RocketMinipoolQueueOld, + RocketNetworkFees, RocketNodeDeposit, RocketNodeStaking, + RocketTokenRETH, } from '../_utils/artifacts'; import { increaseTime } from '../_utils/evm'; import { burnReth } from '../token/scenario-reth-burn'; import { shouldRevert } from '../_utils/testing'; import { assertBN } from '../_helpers/bn'; import { reduceBond } from '../minipool/scenario-reduce-bond'; +import { voteScrub } from '../minipool/scenario-scrub'; +import { close } from '../minipool/scenario-close'; export default function() { @@ -25,7 +34,8 @@ export default function() { const [ owner, node, - trustedNode, + trustedNode1, + trustedNode2, random, ] = accounts; @@ -44,17 +54,29 @@ export default function() { describe('Upgrade Checklist', async () => { // Contracts let rocketDepositPool; + let rocketTokenRETH; + let rocketMinipoolQueue; + let rocketNetworkFees; + let rocketNodeStaking; + let rocketNodeDeposit; // Setup before(async () => { rocketDepositPool = await RocketDepositPool.deployed(); + rocketTokenRETH = await RocketTokenRETH.deployed(); + rocketMinipoolQueue = await RocketMinipoolQueue.deployed(); + rocketNetworkFees = await RocketNetworkFees.deployed(); + rocketNodeStaking = await RocketNodeStaking.deployed(); + rocketNodeDeposit = await RocketNodeDeposit.deployed(); // Register node await registerNode({from: node}); // Register trusted node - await registerNode({from: trustedNode}); - await setNodeTrusted(trustedNode, 'saas_1', 'node@home.com', owner); + await registerNode({from: trustedNode1}); + await setNodeTrusted(trustedNode1, 'saas_1', 'node@home.com', owner); + await registerNode({from: trustedNode2}); + await setNodeTrusted(trustedNode2, 'saas_1', 'node@home.com', owner); // Set settings await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.launch.timeout', launchTimeout, {from: owner}); @@ -64,7 +86,7 @@ export default function() { await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'minipool.bond.reduction.window.length', bondReductionWindowLength, {from: owner}); // Stake RPL to cover minipools - let rplStake = '1360'.ether; + let rplStake = '2000'.ether; await mintRPL(owner, node, rplStake); await nodeStakeRPL(rplStake, {from: node}); @@ -103,10 +125,12 @@ export default function() { }); - it('New Queue Tests (variable queue & queued ETH)', + it('Atlas Testing', async () => { - let variableMinipool1, variableMinipool2, variableMinipool3; + let variableMinipool1, variableMinipool2, variableMinipool3, variableMinipool4, variableMinipool5; + + // New Queue Tests (variable queue & queued ETH) { // Test: Deposit 16 ETH into the deposit pool @@ -182,10 +206,96 @@ export default function() { assertBN.equal(depositPoolBalance, '2'.ether, 'Incorrect deposit pool balance'); } + // Staking + + { + // Test: Wait for legacy minipools to stake + await increaseTime(web3, scrubPeriod); + await stakeMinipool(queuedHalfMinipool1, {from: node}); + await stakeMinipool(queuedHalfMinipool2, {from: node}); + } + + { + // Test: Wait for new 8 ETH minipool to stake + await stakeMinipool(variableMinipool1, {from: node}); + } + + { + // Test: Wait for new 16 ETH minipool to stake + await stakeMinipool(variableMinipool2, {from: node}); + } + + // Dissolves + + { + // Test: Deposit 8 ETH minipool, wait beyond timeout period, node calls close on dissolved pool + await userDeposit({ from: random, value: '29'.ether }); + variableMinipool4 = await createMinipool({ from: node, value: '8'.ether }); + const rethBalance1 = await rocketDepositPool.getBalance(); + await voteScrub(variableMinipool4, {from: trustedNode1}); + await voteScrub(variableMinipool4, {from: trustedNode2}); + const depositBalance2 = await rocketDepositPool.getBalance(); + await close(variableMinipool4, {from: node}); + // Expect: 24 ETH transferred to deposit pool + assertBN.equal(depositBalance2.sub(rethBalance1), '24'.ether, 'Invalid deposit balance'); + } + + { + // Test: Deposit 8 ETH minipool, wait beyond timeout period, node calls close on dissolved pool + variableMinipool5 = await createMinipool({ from: node, value: '16'.ether }); + const rethBalance1 = await rocketDepositPool.getBalance(); + await voteScrub(variableMinipool5, {from: trustedNode1}); + await voteScrub(variableMinipool5, {from: trustedNode2}); + const depositBalance2 = await rocketDepositPool.getBalance(); + await close(variableMinipool5, {from: node}); + // Expect: 16 ETH transferred to deposit pool + assertBN.equal(depositBalance2.sub(rethBalance1), '16'.ether, 'Invalid deposit balance'); + // Empty queue + await burnReth('31'.ether, {from: random}); + } + + // Dynamic Deposit Pool Limit Tests + + { + // Test: Deposit 2x 8 ETH minipools + variableMinipool4 = await createMinipool({ from: node, value: '8'.ether }); + variableMinipool5 = await createMinipool({ from: node, value: '8'.ether }); + // Expected: 2x 8 ETH minipools are in the queue and unassigned, deposit pool contains 14 ETH from node deposits + assertBN.equal(await rocketMinipoolQueue.getTotalLength(), '2', 'Invalid queue length') + assertBN.equal(await rocketDepositPool.getBalance(), '14'.ether, 'Invalid deposit balance'); + } + + { + // Test: Set deposit limit to 1 ETH + await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.pool.maximum', '1'.ether, {from: owner}); + // Expected: Deposit limit set to 1 ETH, dynamic limit should be 49 ETH (1+24*2) + assertBN.equal(await rocketDepositPool.getMaximumDepositAmount(), '49'.ether, 'Invalid maximum deposit amount') + } + + { + // Test: Deposit 50 ETH into deposit pool + // Expected: Deposit should fail + await shouldRevert(userDeposit({ from: random, value: '50'.ether }), 'Was able to deposit more than maximum', 'The deposit pool size after depositing (and matching with minipools) exceeds the maximum size'); + } + + { + // Test: Deposit 49 ETH into deposit pool + // Expected: Deposit should succeed, 2x 8 ETH minipools should be assigned, deposit pool should have 1 ETH remaining + await userDeposit({ from: random, value: '49'.ether }); + assertBN.equal(await rocketMinipoolQueue.getTotalLength(), '0', 'Invalid queue length') + assertBN.equal(await rocketDepositPool.getBalance(), '1'.ether, 'Invalid deposit balance'); + // Reset deposit limit + await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.pool.maximum', '360'.ether, {from: owner}); + } + + // rETH Burn + await userDeposit({ from: random, value: '1'.ether }); + // Note: 1 ETH still remains in the deposit pool from above tests and we just added 1 more so we're at 2 ETH + { // Test: Burn 1 rETH with no minipools in queue + // Expected: rETH burn should succeed await burnReth('1'.ether, {from: random}); - // Expected: rETH burn should success } { @@ -194,8 +304,7 @@ export default function() { // Expected: 8 ETH minipool should be in the queue as initialised, deposit pool should contain 8 ETH let status = await variableMinipool3.getStatus.call(); assertBN.equal(status, minipoolStates.Initialised, 'Incorrect minipool status'); - const depositPoolBalance = await rocketDepositPool.getBalance(); - assertBN.equal(depositPoolBalance, '8'.ether, 'Incorrect deposit pool balance'); + assertBN.equal(await rocketDepositPool.getBalance(), '8'.ether, 'Incorrect deposit pool balance'); } { @@ -210,8 +319,7 @@ export default function() { // Expected: 8 ETH minipool assigned 31 ETH, 1 ETH remaining in the deposit pool let status = await variableMinipool3.getStatus.call(); assertBN.equal(status, minipoolStates.Prelaunch, 'Incorrect minipool status'); - const depositPoolBalance = await rocketDepositPool.getBalance(); - assertBN.equal(depositPoolBalance, '1'.ether, 'Incorrect deposit pool balance'); + assertBN.equal(await rocketDepositPool.getBalance(), '1'.ether, 'Incorrect deposit pool balance'); } { @@ -220,6 +328,121 @@ export default function() { // Expected: rETH burn should success } + // Dynamic Commmission Rate (regression test) + + { + // Test: set a dynamic commission rate + await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.minimum', '0.05'.ether, {from: owner}); + await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.target', '0.10'.ether, {from: owner}); + await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.maximum', '0.20'.ether, {from: owner}); + await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.demand.range', '5'.ether, {from: owner}); + } + + { + // Test: Set deposit limit to 5 ETH + await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.pool.maximum', '5'.ether, {from: owner}); + // Expected: Deposit limit should be 5 ETH + assertBN.equal(await rocketDepositPool.getMaximumDepositAmount(), '5'.ether, 'Invalid maximum deposit amount') + } + + { + // Test: Deposit 5 ETH into deposit pool + await userDeposit({ from: random, value: '5'.ether }); + // Expected: Deposit pool should contain 5 ETH and commission rate (RocketNetworkFees.getNodeFee()) should be at max (20%) + assertBN.equal(await rocketDepositPool.getBalance(), '5'.ether, 'Incorrect deposit pool balance'); + assertBN.equal(await rocketNetworkFees.getNodeFee(), '0.20'.ether, 'Incorrect network node fee'); + } + + { + // Test: Deposit 8 ETH minipool + variableMinipool3 = await createMinipool({ from: node, value: '8'.ether }); + // Expected: 8 ETH minipool should be in the queue, deposit pool balance should be 12 ETH, commission rate should be min (5%) + assertBN.equal(await rocketMinipoolQueue.getTotalLength(), '1', 'Invalid queue length') + assertBN.equal(await rocketDepositPool.getBalance(), '12'.ether, 'Incorrect deposit pool balance'); + assertBN.equal(await rocketNetworkFees.getNodeFee(), '0.05'.ether, 'Incorrect network node fee'); + } + + { + // Test: Deposit 16 ETH into deposit pool + await userDeposit({ from: random, value: '16'.ether }); + // Expected: Deposit pool should contain 28 ETH, commission rate should be 9% + assertBN.equal(await rocketMinipoolQueue.getTotalLength(), '1', 'Invalid queue length') + assertBN.equal(await rocketDepositPool.getBalance(), '28'.ether, 'Incorrect deposit pool balance'); + assertBN.equal(await rocketNetworkFees.getNodeFee(), '0.0892'.ether, 'Incorrect network node fee'); + } + + { + // Test: Deposit 6 ETH into deposit pool + await userDeposit({ from: random, value: '6'.ether }); + // Expected: 8 ETH minipool should be in prelaunch, deposit pool contains 3 ETH, commission rate should be 12% + assertBN.equal(await rocketMinipoolQueue.getTotalLength(), '0', 'Invalid queue length') + assertBN.equal(await rocketDepositPool.getBalance(), '3'.ether, 'Incorrect deposit pool balance'); + assertBN.equal(await rocketNetworkFees.getNodeFee(), '0.1216'.ether, 'Incorrect network node fee'); + } + + { + // Reset commission to 14% + await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.minimum', '0.14'.ether, {from: owner}); + await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.target', '0.14'.ether, {from: owner}); + await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsNetwork, 'network.node.fee.maximum', '0.14'.ether, {from: owner}); + // Burn DP + await burnReth('3'.ether, {from: random}); + } + + // Migrations + + { + // Test: Using a 16 ETH minipool (premigration), execute beginReduceBondAmount, wait timeout period, execute reduceBondAmount + // Expected: Should fail with not enough RPL stake + const minipool = await RocketMinipoolBase.at(queuedHalfMinipool1.address); + await minipool.delegateUpgrade({from: node}); + await queuedHalfMinipool1.beginReduceBondAmount({from: node}); + await increaseTime(web3, bondReductionWindowStart + 1); + await shouldRevert(queuedHalfMinipool1.reduceBondAmount('8'.ether, {from: node}), 'Was able to reduce bond', 'ETH matched after deposit exceeds limit based on node RPL stake'); + } + + { + // Test: Stake RPL then using a 16 ETH minipool (premigration), execute beginReduceBondAmount, wait timeout period, execute reduceBondAmount + let rplStake = '80'.ether; + await mintRPL(owner, node, rplStake); + await nodeStakeRPL(rplStake, {from: node}); + await queuedHalfMinipool1.reduceBondAmount('8'.ether, {from: node}); + // Expected: Node fee should be new fee, deposit type should be variable, node should have a credit for 8 ETH + assertBN.equal(await queuedHalfMinipool1.getNodeFee(), '0.14'.ether, 'Incorrect node fee'); + assertBN.equal(await queuedHalfMinipool1.getDepositType(), '4'.BN, 'Incorrect deposit type'); + assertBN.equal(await rocketNodeDeposit.getNodeDepositCredit(node), '8'.ether, 'Incorrect deposit credit balance'); + } + + { + // Test: Same node deposits new 8 ETH minipool, supplying 0 ETH + // Expected: Should fail with not enough RPL stake + await shouldRevert(createMinipoolWithBondAmount('8'.ether, { from: node, value: '0'.ether }), 'Was able to create new minipool', 'ETH matched after deposit exceeds limit based on node RPL stake'); + } + + { + // Test: Stake RPL then same node deposits new 8 ETH minipool, supplying 0 ETH + let rplStake = '240'.ether; + await mintRPL(owner, node, rplStake); + await nodeStakeRPL(rplStake, {from: node}); + // Expected: Should fail with empty DP + await shouldRevert(createMinipoolWithBondAmount('8'.ether, { from: node, value: '0'.ether }), 'Was able to create new minipool', 'Insufficient contract ETH balance'); + } + + { + // Test: Deposit ETH then stake RPL then same node deposits new 8 ETH minipool, supplying 0 ETH + await userDeposit({ from: random, value: '1'.ether }); + await createMinipoolWithBondAmount('8'.ether, { from: node, value: '0'.ether }); + // Expected: Minipool deposit succeeded, credit should be 0, predeposit 1 ETH taken from deposit pool + assertBN.equal(await rocketMinipoolQueue.getTotalLength(), '1', 'Invalid queue length') + assertBN.equal(await rocketNodeDeposit.getNodeDepositCredit(node), '0'.ether, 'Incorrect deposit credit balance'); + assertBN.equal(await rocketDepositPool.getBalance(), '0'.ether, 'Incorrect deposit pool balance'); + } + + { + // Test: Same node deposits new 8 ETH minipool, supplying 0 ETH + // Expected: Fails, as no more credit + await shouldRevert(createMinipoolWithBondAmount('8'.ether, { from: node, value: '0'.ether }), 'Was able to make new minipool with no credit', 'Invalid value'); + } }); }); @@ -230,8 +453,8 @@ export default function() { await registerNode({ from: node }); // Register trusted node - await registerNode({ from: trustedNode }); - await setNodeTrusted(trustedNode, 'saas_1', 'node@home.com', owner); + await registerNode({ from: trustedNode1 }); + await setNodeTrusted(trustedNode1, 'saas_1', 'node@home.com', owner); // Set settings await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsMinipool, 'minipool.launch.timeout', launchTimeout, { from: owner }); @@ -316,7 +539,7 @@ export default function() { }); - it.only('Handles legacy and variable minipools in the queue simultaneously', async () => { + it('Handles legacy and variable minipools in the queue simultaneously', async () => { // Get contracts const rocketMinipoolQueue = await RocketMinipoolQueue.deployed(); const rocketMinipoolQueueOld = await RocketMinipoolQueueOld.deployed();