diff --git a/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsDeposit.sol b/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsDeposit.sol index 973631bab..19e41e8eb 100644 --- a/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsDeposit.sol +++ b/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsDeposit.sol @@ -20,7 +20,8 @@ contract RocketDAOProtocolSettingsDeposit is RocketDAOProtocolSettings, RocketDA setSettingBool("deposit.assign.enabled", true); setSettingUint("deposit.minimum", 0.01 ether); setSettingUint("deposit.pool.maximum", 160 ether); - setSettingUint("deposit.assign.maximum", 2); + setSettingUint("deposit.assign.maximum", 90); + setSettingUint("deposit.assign.socializedmaximum", 2); setSettingUint("deposit.fee", 0.0005 ether); // Set to approx. 1 day of rewards at 18.25% APR // Settings initialised setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true); @@ -52,6 +53,11 @@ contract RocketDAOProtocolSettingsDeposit is RocketDAOProtocolSettings, RocketDA return getSettingUint("deposit.assign.maximum"); } + // The maximum number of socialized (ie, not related to deposit size) assignments to perform + function getMaximumDepositSocializedAssignments() override external view returns (uint256) { + return getSettingUint("deposit.assign.socializedmaximum"); + } + // Get the fee paid on deposits function getDepositFee() override external view returns (uint256) { return getSettingUint("deposit.fee"); diff --git a/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsMinipool.sol b/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsMinipool.sol index a6aaa28c4..578deb5e3 100644 --- a/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsMinipool.sol +++ b/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsMinipool.sol @@ -8,6 +8,7 @@ import "./RocketDAOProtocolSettings.sol"; import "../../../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol"; import "../../../../interface/dao/node/settings/RocketDAONodeTrustedSettingsMinipoolInterface.sol"; import "../../../../types/MinipoolDeposit.sol"; +import "../../../../contracts/contract/minipool/RocketMinipoolDelegate.sol"; // Network minipool settings contract RocketDAOProtocolSettingsMinipool is RocketDAOProtocolSettings, RocketDAOProtocolSettingsMinipoolInterface { @@ -49,29 +50,25 @@ contract RocketDAOProtocolSettingsMinipool is RocketDAOProtocolSettings, RocketD } // Required node deposit amounts - function getDepositNodeAmount(MinipoolDeposit _depositType) override external pure returns (uint256) { - if (_depositType == MinipoolDeposit.Full) { return getFullDepositNodeAmount(); } - if (_depositType == MinipoolDeposit.Half) { return getHalfDepositNodeAmount(); } - if (_depositType == MinipoolDeposit.Empty) { return getEmptyDepositNodeAmount(); } - return 0; - } function getFullDepositNodeAmount() override public pure returns (uint256) { return getLaunchBalance(); } function getHalfDepositNodeAmount() override public pure returns (uint256) { return getLaunchBalance().div(2); } - function getEmptyDepositNodeAmount() override public pure returns (uint256) { - return 0 ether; - } // Required user deposit amounts function getDepositUserAmount(MinipoolDeposit _depositType) override external pure returns (uint256) { + if (_depositType == MinipoolDeposit.Efficient) { return getEfficientDepositUserAmount(); } if (_depositType == MinipoolDeposit.Full) { return getFullDepositUserAmount(); } if (_depositType == MinipoolDeposit.Half) { return getHalfDepositUserAmount(); } if (_depositType == MinipoolDeposit.Empty) { return getEmptyDepositUserAmount(); } return 0; } + function getEfficientDepositUserAmount() override public pure returns (uint256) { + address delegateAddress = getContractAddress("rocketMinipoolDelegate"); + return getLaunchBalance().sub(delegateAddress.efficientprelaunchAmount); + } function getFullDepositUserAmount() override public pure returns (uint256) { return getLaunchBalance().div(2); } diff --git a/contracts/contract/deposit/RocketDepositPool.sol b/contracts/contract/deposit/RocketDepositPool.sol index 22d95b223..3ad866823 100644 --- a/contracts/contract/deposit/RocketDepositPool.sol +++ b/contracts/contract/deposit/RocketDepositPool.sol @@ -77,7 +77,18 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul require(rocketDAOProtocolSettingsDeposit.getDepositEnabled(), "Deposits into Rocket Pool are currently disabled"); require(msg.value >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit(), "The deposited amount is less than the minimum deposit size"); RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); - require(rocketVault.balanceOf("rocketDepositPool").add(msg.value) <= rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize(), "The deposit pool size after depositing exceeds the maximum size"); + uint256 capacityNeeded = rocketVault.balanceOf("rocketDepositPool").add(msg.value); + if (capacityNeeded > rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize()) { + // Doing a conditional require() instead of a single one optimizes for the common + // case where capacityNeeded fits in the deposit pool without looking at the queue + if (rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { + RocketMinipoolQueueInterface rocketMinipoolQueue = RocketMinipoolQueueInterface(getContractAddress("rocketMinipoolQueue")); + require(capacityNeeded <= rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize() + rocketMinipoolQueue.getEffectiveCapacity(), + "The deposit pool size after depositing (and matching with minipools) exceeds the maximum size"); + } else { + revert("The deposit pool size after depositing exceeds the maximum size"); + } + } // Calculate deposit fee uint256 depositFee = msg.value.mul(rocketDAOProtocolSettingsDeposit.getDepositFee()).div(calcBase); uint256 depositNet = msg.value.sub(depositFee); @@ -140,6 +151,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul } // Assigns deposits to available minipools, returns false if assignment is currently disabled + // Can assign deposits up to the value of the deposit plus getMaximumDepositAssignments() function _assignDeposits(RocketVaultInterface _rocketVault, RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) private returns (bool) { // Check if assigning deposits is enabled if (!_rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { @@ -151,29 +163,74 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul // Setup initial variable values uint256 balance = _rocketVault.balanceOf("rocketDepositPool"); uint256 totalEther = 0; + // Calculate minipool assignments + uint256 i; + uint256 assignmentIndex = 0; + uint256 minipoolCapacity = 0; + uint256 depositValueForAssignments = msg.value; + uint256 socializedAssignmentsLeft = _rocketDAOProtocolSettingsDeposit.getMaximumDepositSocializedAssignments(); uint256 maxAssignments = _rocketDAOProtocolSettingsDeposit.getMaximumDepositAssignments(); MinipoolAssignment[] memory assignments = new MinipoolAssignment[](maxAssignments); - MinipoolDeposit depositType = MinipoolDeposit.None; - uint256 count = 0; - uint256 minipoolCapacity = 0; - for (uint256 i = 0; i < maxAssignments; ++i) { - // Optimised for multiple of the same deposit type - if (count == 0) { - (depositType, count) = rocketMinipoolQueue.getNextDeposit(); - if (depositType == MinipoolDeposit.None) { break; } - minipoolCapacity = rocketDAOProtocolSettingsMinipool.getDepositUserAmount(depositType); + + // Prepare half deposit assignments + minipoolCapacity = rocketDAOProtocolSettingsMinipool.getDepositUserAmount(MinipoolDeposit.Half); + for (i=0; i < rocketMinipoolQueue.getLength(MinipoolDeposit.Half); ++i) { + if (assignmentIndex == maxAssignments) { break; } + if (depositValueForAssignments < minipoolCapacity) { + if (socializedAssignmentsLeft == 0) { break; } + else {socializedAssignmentsLeft--;} + } else { + depositValueForAssignments.sub(minipoolCapacity); } - count--; - if (minipoolCapacity == 0 || balance.sub(totalEther) < minipoolCapacity) { break; } + if (balance.sub(totalEther) < minipoolCapacity) { break; } // Dequeue the minipool - address minipoolAddress = rocketMinipoolQueue.dequeueMinipoolByDeposit(depositType); + address minipoolAddress = rocketMinipoolQueue.dequeueMinipoolByDeposit(MinipoolDeposit.Half); + // Update running total + totalEther = totalEther.add(minipoolCapacity); + // Add assignment, increment index + assignments[assignmentIndex].etherAssigned = minipoolCapacity; + assignments[assignmentIndex].minipoolAddress = minipoolAddress; + assignmentIndex++; + } + + // Prepare full deposit assignments + minipoolCapacity = rocketDAOProtocolSettingsMinipool.getDepositUserAmount(MinipoolDeposit.Full); + for (i=0; i < rocketMinipoolQueue.getLength(MinipoolDeposit.Full); ++i) { + if (assignmentIndex == maxAssignments) { break; } + if (depositValueForAssignments < minipoolCapacity) { + if (socializedAssignmentsLeft == 0) { break; } + else {socializedAssignmentsLeft--;} + } + if (balance.sub(totalEther) < minipoolCapacity) { break; } + // Dequeue the minipool + address minipoolAddress = rocketMinipoolQueue.dequeueMinipoolByDeposit(MinipoolDeposit.Full); + // Update running total + totalEther = totalEther.add(minipoolCapacity); + // Add assignment, increment index + assignments[assignmentIndex].etherAssigned = minipoolCapacity; + assignments[assignmentIndex].minipoolAddress = minipoolAddress; + assignmentIndex++; + } + + // Prepare efficient deposit assignments - will always need 31 ETH + count = rocketMinipoolQueue.getLength(MinipoolDeposit.Efficient); + minipoolCapacity = rocketDAOProtocolSettingsMinipool.getDepositUserAmount(MinipoolDeposit.Efficient); + for (i; i < i + count; ++i) { // NOTE - this is a weird line - we continue the indexing from the full deposit loop + if (depositValueForAssignments < minipoolCapacity) { + if (socializedAssignments == 0) { break; } + else {socializedAssignments--;} + } + if (balance.sub(totalEther) < minipoolCapacity) { break; } + // Dequeue the minipool + address minipoolAddress = rocketMinipoolQueue.dequeueMinipoolByDeposit(MinipoolDeposit.Efficient); // Update running total totalEther = totalEther.add(minipoolCapacity); // Add assignment assignments[i].etherAssigned = minipoolCapacity; assignments[i].minipoolAddress = minipoolAddress; } + if (totalEther > 0) { // Withdraw ETH from vault _rocketVault.withdrawEther(totalEther); diff --git a/contracts/contract/minipool/RocketMinipoolDelegate.sol b/contracts/contract/minipool/RocketMinipoolDelegate.sol index 82341f5ce..43159be2c 100644 --- a/contracts/contract/minipool/RocketMinipoolDelegate.sol +++ b/contracts/contract/minipool/RocketMinipoolDelegate.sol @@ -5,6 +5,7 @@ pragma solidity 0.7.6; import "@openzeppelin/contracts/math/SafeMath.sol"; import "./RocketMinipoolStorageLayout.sol"; +import "../../interface/RocketVaultInterface.sol"; import "../../interface/casper/DepositInterface.sol"; import "../../interface/deposit/RocketDepositPoolInterface.sol"; import "../../interface/minipool/RocketMinipoolInterface.sol"; @@ -17,6 +18,7 @@ import "../../interface/node/RocketNodeStakingInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol"; import "../../interface/dao/node/settings/RocketDAONodeTrustedSettingsMinipoolInterface.sol"; import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol"; +import "../../interface/dao/protocol/settings/RocketDAOProtocolSettingsDepositInterface.sol"; import "../../interface/dao/node/RocketDAONodeTrustedInterface.sol"; import "../../interface/network/RocketNetworkFeesInterface.sol"; import "../../interface/token/RocketTokenRETHInterface.sol"; @@ -31,6 +33,7 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn uint8 public constant version = 2; // Used to identify which delegate contract each minipool is using uint256 constant calcBase = 1 ether; uint256 constant prelaunchAmount = 16 ether; // The amount of ETH initially deposited when minipool is created + uint256 constant efficientprelaunchAmount = 1 ether; // The amount of ETH initially deposited when minipool is created uint256 constant distributionCooldown = 100; // Number of blocks that must pass between calls to distributeBalance // Libs @@ -131,16 +134,20 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn function nodeDeposit(bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) override external payable onlyLatestContract("rocketNodeDeposit", msg.sender) onlyInitialised { // Check current status & node deposit status require(status == MinipoolStatus.Initialised, "The node deposit can only be assigned while initialised"); - require(!nodeDepositAssigned, "The node deposit has already been assigned"); - // Progress full minipool to prelaunch - if (depositType == MinipoolDeposit.Full) { setStatus(MinipoolStatus.Prelaunch); } - // Update node deposit details - nodeDepositBalance = msg.value; - nodeDepositAssigned = true; + require(nodeDepositBalance == 0, "The minipool already has a previous nodeDeposit"); + // Emit ether deposited event emit EtherDeposited(msg.sender, msg.value, block.timestamp); // Perform the pre-stake to lock in withdrawal credentials on beacon chain preStake(_validatorPubkey, _validatorSignature, _depositDataRoot); + + nodeDepositBalance = msg.value; + nodeDepositAssigned = 0; // should be 0 from initialization anyhow + + // Deposit ETH (except the ETH needed to preStake) without minting rETH + // Transfer to vault directly instead of via processDeposits to avoid assigning twice + RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); + rocketVault.depositEther{value: msg.value.sub(efficientprelaunchAmount)}(); } // Assign user deposited ETH to the minipool and mark it as prelaunch @@ -151,15 +158,19 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn require(userDepositAssignedTime == 0, "The user deposit has already been assigned"); // Progress initialised minipool to prelaunch if (status == MinipoolStatus.Initialised) { setStatus(MinipoolStatus.Prelaunch); } - // Update user deposit details - userDepositBalance = msg.value; - userDepositAssignedTime = block.timestamp; - // Refinance full minipool + if (depositType == MinipoolDeposit.Full) { - // Update node balances + // Refinance full minipool nodeDepositBalance = nodeDepositBalance.sub(msg.value); nodeRefundBalance = nodeRefundBalance.add(msg.value); } + + + nodeDepositAssigned = true; // indicate that the node deposit was returned for Efficient queue + // Update user deposit details + userDepositBalance = msg.value; + userDepositAssignedTime = block.timestamp; + // Emit ether deposited event emit EtherDeposited(msg.sender, msg.value, block.timestamp); } @@ -220,7 +231,11 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn DepositInterface casperDeposit = DepositInterface(getContractAddress("casperDeposit")); RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); // Get launch amount - uint256 launchAmount = rocketDAOProtocolSettingsMinipool.getLaunchBalance().sub(prelaunchAmount); + if (depositType == MinipoolDeposit.Efficient) { + uint256 launchAmount = rocketDAOProtocolSettingsMinipool.getLaunchBalance().sub(efficientprelaunchAmount); + } else { + uint256 launchAmount = rocketDAOProtocolSettingsMinipool.getLaunchBalance().sub(prelaunchAmount); + } // Check minipool balance require(address(this).balance >= launchAmount, "Insufficient balance to begin staking"); // Retrieve validator pubkey from storage @@ -237,7 +252,7 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn DepositInterface casperDeposit = DepositInterface(getContractAddress("casperDeposit")); RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager")); // Check minipool balance - require(address(this).balance >= prelaunchAmount, "Insufficient balance to pre-stake"); + require(address(this).balance >= efficientprelaunchAmount, "Insufficient balance to pre-stake"); // Check validator pubkey is not in use require(rocketMinipoolManager.getMinipoolByPubkey(_validatorPubkey) == address(0x0), "Validator pubkey is in use"); // Set minipool pubkey @@ -245,9 +260,9 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn // Get withdrawal credentials bytes memory withdrawalCredentials = rocketMinipoolManager.getMinipoolWithdrawalCredentials(address(this)); // Send staking deposit to casper - casperDeposit.deposit{value : prelaunchAmount}(_validatorPubkey, withdrawalCredentials, _validatorSignature, _depositDataRoot); + casperDeposit.deposit{value : efficientprelaunchAmount}(_validatorPubkey, withdrawalCredentials, _validatorSignature, _depositDataRoot); // Emit event - emit MinipoolPrestaked(_validatorPubkey, _validatorSignature, _depositDataRoot, prelaunchAmount, withdrawalCredentials, block.timestamp); + emit MinipoolPrestaked(_validatorPubkey, _validatorSignature, _depositDataRoot, efficientprelaunchAmount, withdrawalCredentials, block.timestamp); } // Mark the minipool as withdrawable @@ -420,15 +435,15 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn } // Dissolve the minipool, returning user deposited ETH to the deposit pool - // Only accepts calls from the minipool owner (node), or from any address if timed out + // Only accepts calls when in Prelaunch for too long without calling stake() + // In other words, this prevents User ETH from getting stuck when an NO fails to move forward function dissolve() override external onlyInitialised { // Check current status - require(status == MinipoolStatus.Initialised || status == MinipoolStatus.Prelaunch, "The minipool can only be dissolved while initialised or in prelaunch"); + require(status == MinipoolStatus.Prelaunch, "The minipool can only be dissolved while in prelaunch"); // Load contracts RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); - // Check if being dissolved by minipool owner or minipool is timed out require( - (status == MinipoolStatus.Prelaunch && block.timestamp.sub(statusTime) >= rocketDAOProtocolSettingsMinipool.getLaunchTimeout()), + (block.timestamp.sub(statusTime) >= rocketDAOProtocolSettingsMinipool.getLaunchTimeout()), "The minipool can only be dissolved once it has timed out" ); // Perform the dissolution diff --git a/contracts/contract/minipool/RocketMinipoolQueue.sol b/contracts/contract/minipool/RocketMinipoolQueue.sol index 9afcd055b..929f349c0 100644 --- a/contracts/contract/minipool/RocketMinipoolQueue.sol +++ b/contracts/contract/minipool/RocketMinipoolQueue.sol @@ -47,6 +47,7 @@ contract RocketMinipoolQueue is RocketBase, RocketMinipoolQueueInterface { // Get the length of a queue // Returns 0 for invalid queues function getLength(MinipoolDeposit _depositType) override external view returns (uint256) { + if (_depositType == MinipoolDeposit.Efficient) { return getLength(queueKeyEfficient); } if (_depositType == MinipoolDeposit.Full) { return getLength(queueKeyFull); } if (_depositType == MinipoolDeposit.Half) { return getLength(queueKeyHalf); } if (_depositType == MinipoolDeposit.Empty) { return getLength(queueKeyEmpty); } @@ -57,18 +58,6 @@ contract RocketMinipoolQueue is RocketBase, RocketMinipoolQueueInterface { return addressQueueStorage.getLength(_key); } - // Get the total combined capacity of the queues - function getTotalCapacity() override external view returns (uint256) { - RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); - return ( - getLength(queueKeyFull).mul(rocketDAOProtocolSettingsMinipool.getFullDepositUserAmount()) - ).add( - getLength(queueKeyHalf).mul(rocketDAOProtocolSettingsMinipool.getHalfDepositUserAmount()) - ).add( - getLength(queueKeyEmpty).mul(rocketDAOProtocolSettingsMinipool.getEmptyDepositUserAmount()) - ); - } - // Get the total effective capacity of the queues (used in node demand calculation) function getEffectiveCapacity() override external view returns (uint256) { RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); @@ -76,34 +65,15 @@ contract RocketMinipoolQueue is RocketBase, RocketMinipoolQueueInterface { getLength(queueKeyFull).mul(rocketDAOProtocolSettingsMinipool.getFullDepositUserAmount()) ).add( getLength(queueKeyHalf).mul(rocketDAOProtocolSettingsMinipool.getHalfDepositUserAmount()) + ).add( + getLength(queueKeyEfficient).mul(rocketDAOProtocolSettingsMinipool.getEfficientDepositUserAmount()) ); } - // Get the capacity of the next available minipool - // Returns 0 if no minipools are available - function getNextCapacity() override external view returns (uint256) { - RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); - if (getLength(queueKeyHalf) > 0) { return rocketDAOProtocolSettingsMinipool.getHalfDepositUserAmount(); } - if (getLength(queueKeyFull) > 0) { return rocketDAOProtocolSettingsMinipool.getFullDepositUserAmount(); } - if (getLength(queueKeyEmpty) > 0) { return rocketDAOProtocolSettingsMinipool.getEmptyDepositUserAmount(); } - return 0; - } - - // Get the deposit type of the next available minipool and the number of deposits in that queue - // Returns None if no minipools are available - function getNextDeposit() override external view returns (MinipoolDeposit, uint256) { - uint256 length = getLength(queueKeyHalf); - if (length > 0) { return (MinipoolDeposit.Half, length); } - length = getLength(queueKeyFull); - if (length > 0) { return (MinipoolDeposit.Full, length); } - length = getLength(queueKeyEmpty); - if (length > 0) { return (MinipoolDeposit.Empty, length); } - return (MinipoolDeposit.None, 0); - } - // Add a minipool to the end of the appropriate queue // Only accepts calls from the RocketMinipoolManager contract function enqueueMinipool(MinipoolDeposit _depositType, address _minipool) override external onlyLatestContract("rocketMinipoolQueue", address(this)) onlyLatestContract("rocketMinipoolManager", msg.sender) { + if (_depositType == MinipoolDeposit.Efficient) { return enqueueMinipool(queueKeyEfficient, _minipool); } if (_depositType == MinipoolDeposit.Half) { return enqueueMinipool(queueKeyHalf, _minipool); } if (_depositType == MinipoolDeposit.Full) { return enqueueMinipool(queueKeyFull, _minipool); } if (_depositType == MinipoolDeposit.Empty) { return enqueueMinipool(queueKeyEmpty, _minipool); } @@ -117,15 +87,10 @@ contract RocketMinipoolQueue is RocketBase, RocketMinipoolQueueInterface { emit MinipoolEnqueued(_minipool, _key, block.timestamp); } - // Remove the first available minipool from the highest priority queue and return its address // Only accepts calls from the RocketDepositPool contract - function dequeueMinipool() override external onlyLatestContract("rocketMinipoolQueue", address(this)) onlyLatestContract("rocketDepositPool", msg.sender) returns (address minipoolAddress) { - if (getLength(queueKeyHalf) > 0) { return dequeueMinipool(queueKeyHalf); } - if (getLength(queueKeyFull) > 0) { return dequeueMinipool(queueKeyFull); } - if (getLength(queueKeyEmpty) > 0) { return dequeueMinipool(queueKeyEmpty); } - require(false, "No minipools are available"); } function dequeueMinipoolByDeposit(MinipoolDeposit _depositType) override external onlyLatestContract("rocketMinipoolQueue", address(this)) onlyLatestContract("rocketDepositPool", msg.sender) returns (address minipoolAddress) { + if (_depositType == MinipoolDeposit.Efficient) { return dequeueMinipool(queueKeyEfficient); } if (_depositType == MinipoolDeposit.Half) { return dequeueMinipool(queueKeyHalf); } if (_depositType == MinipoolDeposit.Full) { return dequeueMinipool(queueKeyFull); } if (_depositType == MinipoolDeposit.Empty) { return dequeueMinipool(queueKeyEmpty); } @@ -143,8 +108,11 @@ contract RocketMinipoolQueue is RocketBase, RocketMinipoolQueueInterface { // Remove a minipool from a queue // Only accepts calls from registered minipools + // Note: this removal is made computationally efficient by swapping with the last item in the + // queue. This is acceptable because removing minipools should be rare. function removeMinipool(MinipoolDeposit _depositType) override external onlyLatestContract("rocketMinipoolQueue", address(this)) onlyRegisteredMinipool(msg.sender) { // Remove minipool from queue + if (_depositType == MinipoolDeposit.Efficient) { return removeMinipool(queueKeyEfficient, msg.sender); } if (_depositType == MinipoolDeposit.Half) { return removeMinipool(queueKeyHalf, msg.sender); } if (_depositType == MinipoolDeposit.Full) { return removeMinipool(queueKeyFull, msg.sender); } if (_depositType == MinipoolDeposit.Empty) { return removeMinipool(queueKeyEmpty, msg.sender); } diff --git a/contracts/contract/network/RocketNetworkBalances.sol b/contracts/contract/network/RocketNetworkBalances.sol index 8e855ca4f..4a08aacb3 100644 --- a/contracts/contract/network/RocketNetworkBalances.sol +++ b/contracts/contract/network/RocketNetworkBalances.sol @@ -17,7 +17,7 @@ contract RocketNetworkBalances is RocketBase, RocketNetworkBalancesInterface { using SafeMath for uint; // Events - event BalancesSubmitted(address indexed from, uint256 block, uint256 totalEth, uint256 stakingEth, uint256 rethSupply, uint256 time); + event BalancesSubmitted(address indexed from, uint256 block, uint256 totalEth, uint256 stakingEth, uint256 stakingQueueEth, uint256 rethSupply, uint256 time); event BalancesUpdated(uint256 block, uint256 totalEth, uint256 stakingEth, uint256 rethSupply, uint256 time); // Construct @@ -33,7 +33,7 @@ contract RocketNetworkBalances is RocketBase, RocketNetworkBalancesInterface { setUint(keccak256("network.balances.updated.block"), _value); } - // The current RP network total ETH balance + // The current RP network total user ETH balance function getTotalETHBalance() override public view returns (uint256) { return getUint(keccak256("network.balance.total")); } @@ -41,7 +41,7 @@ contract RocketNetworkBalances is RocketBase, RocketNetworkBalancesInterface { setUint(keccak256("network.balance.total"), _value); } - // The current RP network staking ETH balance + // The current RP network staking ETH balance from users function getStakingETHBalance() override public view returns (uint256) { return getUint(keccak256("network.balance.staking")); } @@ -49,6 +49,15 @@ contract RocketNetworkBalances is RocketBase, RocketNetworkBalancesInterface { setUint(keccak256("network.balance.staking"), _value); } + // The current RP network total staking ETH balance from the minipool queue + // - Note: this does not contribute to getTotalETHBalance, as it isn't from User deposits + function getStakingQueueETHBalance() override public view returns (uint256) { + return getUint(keccak256("network.balance.queuestaking")); + } + function setStakingQueueETHBalance(uint256 _value) private { + setUint(keccak256("network.balance.queuestaking"), _value); + } + // The current RP network total rETH supply function getTotalRETHSupply() override external view returns (uint256) { return getUint(keccak256("network.balance.reth.supply")); @@ -68,7 +77,7 @@ contract RocketNetworkBalances is RocketBase, RocketNetworkBalancesInterface { // Submit network balances for a block // Only accepts calls from trusted (oracle) nodes - function submitBalances(uint256 _block, uint256 _totalEth, uint256 _stakingEth, uint256 _rethSupply) override external onlyLatestContract("rocketNetworkBalances", address(this)) onlyTrustedNode(msg.sender) { + function submitBalances(uint256 _block, uint256 _totalEth, uint256 _stakingEth, uint256 _stakingQueueEth, uint256 _rethSupply) override external onlyLatestContract("rocketNetworkBalances", address(this)) onlyTrustedNode(msg.sender) { // Check settings RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); require(rocketDAOProtocolSettingsNetwork.getSubmitBalancesEnabled(), "Submitting balances is currently disabled"); @@ -78,8 +87,8 @@ contract RocketNetworkBalances is RocketBase, RocketNetworkBalancesInterface { // Check balances require(_stakingEth <= _totalEth, "Invalid network balances"); // Get submission keys - bytes32 nodeSubmissionKey = keccak256(abi.encodePacked("network.balances.submitted.node", msg.sender, _block, _totalEth, _stakingEth, _rethSupply)); - bytes32 submissionCountKey = keccak256(abi.encodePacked("network.balances.submitted.count", _block, _totalEth, _stakingEth, _rethSupply)); + bytes32 nodeSubmissionKey = keccak256(abi.encodePacked("network.balances.submitted.node", msg.sender, _block, _totalEth, _stakingEth, _stakingQueueEth, _rethSupply)); + bytes32 submissionCountKey = keccak256(abi.encodePacked("network.balances.submitted.count", _block, _totalEth, _stakingEth, _stakingQueueEth, _rethSupply)); // Check & update node submission status require(!getBool(nodeSubmissionKey), "Duplicate submission from node"); setBool(nodeSubmissionKey, true); @@ -88,16 +97,16 @@ contract RocketNetworkBalances is RocketBase, RocketNetworkBalancesInterface { uint256 submissionCount = getUint(submissionCountKey).add(1); setUint(submissionCountKey, submissionCount); // Emit balances submitted event - emit BalancesSubmitted(msg.sender, _block, _totalEth, _stakingEth, _rethSupply, block.timestamp); + emit BalancesSubmitted(msg.sender, _block, _totalEth, _stakingEth, _stakingQueueEth, _rethSupply, block.timestamp); // Check submission count & update network balances RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); if (calcBase.mul(submissionCount).div(rocketDAONodeTrusted.getMemberCount()) >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold()) { - updateBalances(_block, _totalEth, _stakingEth, _rethSupply); + updateBalances(_block, _totalEth, _stakingEth, _stakingQueueEth, _rethSupply); } } // Executes updateBalances if consensus threshold is reached - function executeUpdateBalances(uint256 _block, uint256 _totalEth, uint256 _stakingEth, uint256 _rethSupply) override external onlyLatestContract("rocketNetworkBalances", address(this)) { + function executeUpdateBalances(uint256 _block, uint256 _totalEth, uint256 _stakingEth, uint256, _stakingQueueEth, uint256 _rethSupply) override external onlyLatestContract("rocketNetworkBalances", address(this)) { // Check settings RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress("rocketDAOProtocolSettingsNetwork")); require(rocketDAOProtocolSettingsNetwork.getSubmitBalancesEnabled(), "Submitting balances is currently disabled"); @@ -113,7 +122,7 @@ contract RocketNetworkBalances is RocketBase, RocketNetworkBalancesInterface { // Check submission count & update network balances RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress("rocketDAONodeTrusted")); require(calcBase.mul(submissionCount).div(rocketDAONodeTrusted.getMemberCount()) >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold(), "Consensus has not been reached"); - updateBalances(_block, _totalEth, _stakingEth, _rethSupply); + updateBalances(_block, _totalEth, _stakingEth, _stakingQueueEth, _rethSupply); } // Update network balances @@ -122,9 +131,10 @@ contract RocketNetworkBalances is RocketBase, RocketNetworkBalancesInterface { setBalancesBlock(_block); setTotalETHBalance(_totalEth); setStakingETHBalance(_stakingEth); + setStakingQueueETHBalance(_stakingQueueEth); setTotalRETHSupply(_rethSupply); // Emit balances updated event - emit BalancesUpdated(_block, _totalEth, _stakingEth, _rethSupply, block.timestamp); + emit BalancesUpdated(_block, _totalEth, _stakingEth, _stakingQueueEth, _rethSupply, block.timestamp); } // Returns the latest block number that oracles should be reporting balances for diff --git a/contracts/contract/node/RocketNodeDeposit.sol b/contracts/contract/node/RocketNodeDeposit.sol index 26f79a271..f3daa2c36 100644 --- a/contracts/contract/node/RocketNodeDeposit.sol +++ b/contracts/contract/node/RocketNodeDeposit.sol @@ -70,11 +70,13 @@ contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { function getDepositType(uint256 _amount) public override view returns (MinipoolDeposit) { // Get contract RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); - // Get deposit type by node deposit amount - if (_amount == rocketDAOProtocolSettingsMinipool.getFullDepositNodeAmount()) { return MinipoolDeposit.Full; } - else if (_amount == rocketDAOProtocolSettingsMinipool.getHalfDepositNodeAmount()) { return MinipoolDeposit.Half; } - // Invalid deposit amount - return MinipoolDeposit.None; + // Ensure valid deposit amount + if (_amount == rocketDAOProtocolSettingsMinipool.getHalfDepositNodeAmount()) { + // invalid deposit amount + return MinipoolDeposit.None; + } + // All deposits going forward have the same type and use the same queue + return MinipoolDeposit.Efficient; } function checkNodeFee(uint256 _minimumNodeFee) private view { diff --git a/contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol b/contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol index 4fee3d256..31f9986bf 100644 --- a/contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol +++ b/contracts/interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol @@ -6,7 +6,6 @@ import "../../../../types/MinipoolDeposit.sol"; interface RocketDAOProtocolSettingsMinipoolInterface { function getLaunchBalance() external view returns (uint256); - function getDepositNodeAmount(MinipoolDeposit _depositType) external view returns (uint256); function getFullDepositNodeAmount() external view returns (uint256); function getHalfDepositNodeAmount() external view returns (uint256); function getEmptyDepositNodeAmount() external view returns (uint256); diff --git a/contracts/interface/minipool/RocketMinipoolQueueInterface.sol b/contracts/interface/minipool/RocketMinipoolQueueInterface.sol index 326690d86..3781427f6 100644 --- a/contracts/interface/minipool/RocketMinipoolQueueInterface.sol +++ b/contracts/interface/minipool/RocketMinipoolQueueInterface.sol @@ -7,10 +7,7 @@ import "../../types/MinipoolDeposit.sol"; interface RocketMinipoolQueueInterface { function getTotalLength() external view returns (uint256); function getLength(MinipoolDeposit _depositType) external view returns (uint256); - function getTotalCapacity() external view returns (uint256); function getEffectiveCapacity() external view returns (uint256); - function getNextCapacity() external view returns (uint256); - function getNextDeposit() external view returns (MinipoolDeposit, uint256); function enqueueMinipool(MinipoolDeposit _depositType, address _minipool) external; function dequeueMinipool() external returns (address minipoolAddress); function dequeueMinipoolByDeposit(MinipoolDeposit _depositType) external returns (address minipoolAddress); diff --git a/design.md b/design.md new file mode 100644 index 000000000..efec50af0 --- /dev/null +++ b/design.md @@ -0,0 +1,17 @@ +## Top level idea + +- Consistency makes our queue easier to work with + - For example, queue_capacity is currently just queue_length*16 for the half deposit queue + - For example, we need to know the capacity of the next minipool in the queue to decide whether we + have enough ETH to assign it +- We can maintain consistency by implementing the "use eth in the queue" idea + - For this idea, we make the minimum deposit on the beacon chain (1 ETH), and _every_ minipool + will need 31 ETH to launch. This will be true with 16 or 8 ETH node deposits for now, and for + whatever future values we support. + - If we _don't_ maintain consistency like this, finding capacity requires either iterating through + the queue, or adding an extra tracking variable that we update on pop. +- In my "[wip] Deposit side" commit, I've used nodeDepositAssigned=False, and a positive + nodeDepositBalance to indicate this state where I've provided some ETH (eg, 16), but it's not all + in the minipool contract. + +To add LEB8s, we'd need to tweak getDepositType to allow 8 ETH deposits. \ No newline at end of file diff --git a/test/deposit/scenario-assign-deposits.js b/test/deposit/scenario-assign-deposits.js index b518c88c3..17bc1e100 100644 --- a/test/deposit/scenario-assign-deposits.js +++ b/test/deposit/scenario-assign-deposits.js @@ -66,10 +66,9 @@ export async function assignDeposits(txOptions) { function getMinipoolQueueDetails() { return Promise.all([ rocketMinipoolQueue.getTotalLength.call(), - rocketMinipoolQueue.getTotalCapacity.call(), ]).then( - ([totalLength, totalCapacity]) => - ({totalLength, totalCapacity}) + ([totalLength]) => + ({totalLength}) ); } @@ -94,7 +93,6 @@ export async function assignDeposits(txOptions) { // Check minipool queues assert(queue2.totalLength.eq(queue1.totalLength.sub(web3.utils.toBN(expectedDepositAssignments))), 'Incorrect updated minipool queue length'); - assert(queue2.totalCapacity.eq(queue1.totalCapacity.sub(expectedEthAssigned)), 'Incorrect updated minipool queue capacity'); }