From 2b4196f721e5fb5320159ae3b7722f253fa36e0d Mon Sep 17 00:00:00 2001 From: Kane Wallmann Date: Tue, 21 Jan 2025 15:07:30 +1000 Subject: [PATCH] Work on deposit queue exit --- .../contract/deposit/RocketDepositPool.sol | 130 ++++++++++-------- .../megapool/RocketMegapoolDelegate.sol | 12 +- contracts/contract/node/RocketNodeDeposit.sol | 6 +- contracts/contract/util/LinkedListStorage.sol | 20 +-- .../contract/util/LinkedListStorageHelper.sol | 6 +- .../deposit/RocketDepositPoolInterface.sol | 5 +- .../util/LinkedListStorageInterface.sol | 9 +- test/megapool/megapool-tests.js | 16 ++- test/megapool/scenario-exit-queue.js | 25 ++++ 9 files changed, 141 insertions(+), 88 deletions(-) create mode 100644 test/megapool/scenario-exit-queue.js diff --git a/contracts/contract/deposit/RocketDepositPool.sol b/contracts/contract/deposit/RocketDepositPool.sol index 9f784ac6..4099f373 100644 --- a/contracts/contract/deposit/RocketDepositPool.sol +++ b/contracts/contract/deposit/RocketDepositPool.sol @@ -18,15 +18,16 @@ import "../../types/MinipoolDeposit.sol"; import "../RocketBase.sol"; import "../../interface/node/RocketNodeStakingInterface.sol"; import "../../interface/megapool/RocketMegapoolFactoryInterface.sol"; +import {RocketNodeManager} from "../node/RocketNodeManager.sol"; /// @notice Accepts user deposits and mints rETH; handles assignment of deposited ETH to megapools contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaultWithdrawerInterface { // Constants - uint256 private constant milliToWei = 10 ** 15; - bytes32 private constant queueKeyVariable = keccak256("minipools.available.variable"); - bytes32 private constant expressQueueNamespace = keccak256("deposit.queue.express"); - bytes32 private constant standardQueueNamespace = keccak256("deposit.queue.standard"); + uint256 internal constant milliToWei = 10 ** 15; + bytes32 internal constant queueKeyVariable = keccak256("minipools.available.variable"); + bytes32 internal constant expressQueueNamespace = keccak256("deposit.queue.express"); + bytes32 internal constant standardQueueNamespace = keccak256("deposit.queue.standard"); // Immutables RocketVaultInterface immutable internal rocketVault; @@ -40,6 +41,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul event FundsRequested(address indexed receiver, uint256 validatorId, uint256 amount, bool expressQueue, uint256 time); event FundsAssigned(address indexed receiver, uint256 amount, uint256 time); event QueueExited(address indexed receiver, uint256 time); + event CreditWithdrawn(address indexed receiver, uint256 amount, uint256 time); // Structs struct MinipoolAssignment { @@ -71,12 +73,19 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul return getUint("deposit.pool.node.balance"); } - /// @notice Returns the user owned portion of the deposit pool (negative indicates more ETH has been "lent" to the - /// deposit pool by node operators in the queue than is available from user deposits) + /// @notice Returns the user owned portion of the deposit pool + /// @dev Negative indicates more ETH has been "lent" to the deposit pool by node operators in the queue + /// than is available from user deposits function getUserBalance() override public view returns (int256) { return int256(getBalance()) - int256(getNodeBalance()); } + /// @notice Returns the credit balance for a given node operator + /// @param _nodeAddress Address of the node operator to query + function getNodeCreditBalance(address _nodeAddress) override public view returns (uint256) { + return getUint(keccak256(abi.encodePacked("deposit.pool.node.credit", _nodeAddress))); + } + /// @notice Excess deposit pool balance (in excess of minipool queue capacity) function getExcessBalance() override public view returns (uint256) { // Get minipool queue capacity @@ -115,7 +124,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul if (rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { RocketMinipoolQueueInterface rocketMinipoolQueue = RocketMinipoolQueueInterface(getContractAddress("rocketMinipoolQueue")); uint256 capacity = rocketMinipoolQueue.getEffectiveCapacity(); - capacity += getUint("deposit.pool.requested.total"); + capacity += getUint(keccak256("deposit.pool.requested.total")); require(capacityNeeded <= maxDepositPoolSize + capacity, "The deposit pool size after depositing exceeds the maximum size"); } else { revert("The deposit pool size after depositing exceeds the maximum size"); @@ -145,7 +154,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul if (rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { RocketMinipoolQueueInterface rocketMinipoolQueue = RocketMinipoolQueueInterface(getContractAddress("rocketMinipoolQueue")); maxCapacity += rocketMinipoolQueue.getEffectiveCapacity(); - maxCapacity += getUint("deposit.pool.requested.total"); + maxCapacity += getUint(keccak256("deposit.pool.requested.total")); } // Check we aren't already over if (depositPoolBalance >= maxCapacity) { @@ -154,47 +163,34 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul return maxCapacity - depositPoolBalance; } - /// @dev Accepts ETH deposit from the node deposit contract (does not mint rETH) - /// @param _totalAmount The total node deposit amount including any credit balance used - function nodeDeposit(uint256 _totalAmount) override external payable onlyThisLatestContract onlyLatestContract("rocketNodeDeposit", msg.sender) { + /// @notice Accepts ETH deposit from the node deposit contract (does not mint rETH) + /// @param _bondAmount The total node deposit amount including any credit balance used + function nodeDeposit(uint256 _bondAmount) override external payable onlyThisLatestContract onlyLatestContract("rocketNodeDeposit", msg.sender) { // Deposit ETH into the vault if (msg.value > 0) { rocketVault.depositEther{value: msg.value}(); } // Increase recorded node balance - addUint("deposit.pool.node.balance", _totalAmount); - } - - /// @dev Withdraws ETH from the deposit pool to RocketNodeDeposit contract to be used for a new minipool - /// @param _amount The amount of ETH to withdraw - function nodeCreditWithdrawal(uint256 _amount) override external onlyThisLatestContract onlyLatestContract("rocketNodeDeposit", msg.sender) { - // Withdraw ETH from the vault - rocketVault.withdrawEther(_amount); - // Send it to msg.sender (function modifier verifies msg.sender is RocketNodeDeposit) - (bool success,) = address(msg.sender).call{value: _amount}(""); - require(success, "Failed to send ETH"); + addUint("deposit.pool.node.balance", _bondAmount); } - /// @dev Recycle a deposit from a dissolved validator + /// @notice Recycle a deposit from a dissolved validator function recycleDissolvedDeposit() override external payable onlyThisLatestContract onlyRegisteredMinipoolOrMegapool(msg.sender) { - // Load contracts - RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); - // Recycle ETH - emit DepositRecycled(msg.sender, msg.value, block.timestamp); - processDeposit(rocketDAOProtocolSettingsDeposit); + _recycleValue(); } - /// @dev Recycle excess ETH from the rETH token contract + /// @notice Recycle excess ETH from the rETH token contract function recycleExcessCollateral() override external payable onlyThisLatestContract onlyLatestContract("rocketTokenRETH", msg.sender) { - // Load contracts - RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); - // Recycle ETH - emit DepositRecycled(msg.sender, msg.value, block.timestamp); - processDeposit(rocketDAOProtocolSettingsDeposit); + _recycleValue(); } - /// @dev Recycle a liquidated RPL stake from a slashed minipool + /// @notice Recycle a liquidated RPL stake from a slashed minipool function recycleLiquidatedStake() override external payable onlyThisLatestContract onlyLatestContract("rocketAuctionManager", msg.sender) { + _recycleValue(); + } + + /// @dev Recycles msg.value into the deposit pool + function _recycleValue() internal { // Load contracts RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); // Recycle ETH @@ -202,8 +198,8 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul processDeposit(rocketDAOProtocolSettingsDeposit); } - /// @dev Process a deposit - function processDeposit(RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) private { + /// @dev Deposits incoming funds into vault and performs assignment + function processDeposit(RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) internal { // Transfer ETH to vault rocketVault.depositEther{value: msg.value}(); // Assign deposits if enabled @@ -218,7 +214,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul require(_assignDeposits(rocketDAOProtocolSettingsDeposit), "Deposit assignments are currently disabled"); } - /// @dev Assign deposits to available minipools. Does nothing if assigning deposits is disabled. + /// @notice Assign deposits to available minipools. Does nothing if assigning deposits is disabled. function maybeAssignDeposits() override external onlyThisLatestContract returns (bool) { // Load contracts RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); @@ -226,7 +222,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul return _assignDeposits(rocketDAOProtocolSettingsDeposit); } - /// @dev If deposit assignments are enabled, assigns a single deposit + /// @notice If deposit assignments are enabled, assigns a single deposit function maybeAssignOneDeposit() override external onlyThisLatestContract { RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); if (!rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { @@ -240,12 +236,12 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul /// @param _count The maximum number of megapools to assign in this call function assignMegapools(uint256 _count) override external onlyThisLatestContract { RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit")); - require (rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled(), "Deposit assignments are disabled"); + require(rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled(), "Deposit assignments are disabled"); _assignMegapools(_count); } /// @dev Assigns deposits to available minipools, returns false if assignment is currently disabled - function _assignDeposits(RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) private returns (bool) { + function _assignDeposits(RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) internal returns (bool) { // Check if assigning deposits is enabled if (!_rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { return false; @@ -263,7 +259,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul } /// @dev Assigns deposits using the new megapool queue - function _assignDepositsNew(RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) private { + function _assignDepositsNew(RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) internal { // Load contracts RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); // Calculate the number of minipools to assign @@ -278,7 +274,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul } /// @dev Assigns deposits using the legacy minipool queue - function _assignDepositsLegacy(RocketMinipoolQueueInterface _rocketMinipoolQueue, RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) private { + function _assignDepositsLegacy(RocketMinipoolQueueInterface _rocketMinipoolQueue, RocketDAOProtocolSettingsDepositInterface _rocketDAOProtocolSettingsDeposit) internal { // Load contracts RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress("rocketDAOProtocolSettingsMinipool")); // Calculate the number of minipools to assign @@ -330,7 +326,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul /// @param _validatorId The megapool-managed ID of the validator requesting funds /// @param _amount The amount of ETH requested by the node operator /// @param _expressQueue Whether to consume an express ticket to be placed in the express queue - function requestFunds(uint256 _bondAmount, uint256 _validatorId, uint256 _amount, bool _expressQueue) external payable onlyRegisteredMegapool(msg.sender) { + function requestFunds(uint256 _bondAmount, uint256 _validatorId, uint256 _amount, bool _expressQueue) external onlyRegisteredMegapool(msg.sender) { // Validate arguments require(_bondAmount % milliToWei == 0, "Invalid supplied amount"); require(_amount % milliToWei == 0, "Invalid requested amount"); @@ -350,28 +346,52 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul }); LinkedListStorageInterface linkedListStorage = LinkedListStorageInterface(getContractAddress("linkedListStorage")); linkedListStorage.enqueueItem(namespace, value); - // Increase requested balance - addUint("deposit.pool.requested.total", _amount); + // Increase requested balance and node balance + addUint(keccak256("deposit.pool.requested.total"), _amount); // Emit event emit FundsRequested(msg.sender, _validatorId, _amount, _expressQueue, block.timestamp); } - /// @notice Removes a pending entry in the validator queue + /// @notice Removes a pending entry in the validator queue and returns funds to node by credit mechanism /// @param _validatorId Internal ID of the validator to be removed /// @param _expressQueue Whether the entry is in the express queue or not function exitQueue(uint256 _validatorId, bool _expressQueue) external onlyRegisteredMegapool(msg.sender) { LinkedListStorageInterface linkedListStorage = LinkedListStorageInterface(getContractAddress("linkedListStorage")); - DepositQueueValue memory key = DepositQueueValue({ + DepositQueueKey memory key = DepositQueueKey({ receiver: msg.sender, - validatorId: uint32(_validatorId), - suppliedValue: 0, - requestedValue: 0 + validatorId: uint32(_validatorId) }); bytes32 namespace = getQueueNamespace(_expressQueue); + uint256 index = linkedListStorage.getIndexOf(namespace, key); + DepositQueueValue memory value = linkedListStorage.getItem(namespace, index); linkedListStorage.removeItem(namespace, key); + // Perform balance accounting + subUint(keccak256("deposit.pool.requested.total"), value.requestedValue * milliToWei); + // Add to node's credit for the amount supplied + RocketMegapoolDelegateInterface megapool = RocketMegapoolDelegateInterface(msg.sender); + address nodeAddress = megapool.getNodeAddress(); + addUint(keccak256(abi.encodePacked("deposit.pool.node.credit", nodeAddress)), value.suppliedValue * milliToWei); + // TODO: Should node operator receive their express ticket back? emit QueueExited(msg.sender, block.timestamp); } + /// @notice Allows node operator to withdraw any ETH credit they have as rETH + /// @param _amount Amount in ETH to withdraw + function withdrawCredit(uint256 _amount) override external onlyRegisteredNode(msg.sender) { + uint256 credit = getUint(keccak256(abi.encodePacked("deposit.pool.node.credit", msg.sender))); + require(credit >= _amount, "Amount exceeds credit available"); + // Account for balance changes + subUint(keccak256(abi.encodePacked("deposit.pool.node.credit", msg.sender)), _amount); + subUint("deposit.pool.node.balance", _amount); + // Mint rETH to node + // TODO: Do we need to check deposits are enabled, capacity is respected and apply a deposit fee? + RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress("rocketNodeManager")); + rocketTokenRETH.mint(_amount, rocketNodeManager.getNodeWithdrawalAddress(msg.sender)); + // The funds are already stored in RocketVault under RocketDepositPool so no transfer is required + // Emit event + emit CreditWithdrawn(msg.sender, _amount, block.timestamp); + } + /// @notice Gets the receiver next to be assigned and whether it can be assigned immediately /// @dev During the transition period from the legacy minipool queue, this will always return null address /// @return receiver Address of the receiver of the next assignment or null address for an empty queue @@ -394,7 +414,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul return (address(0x0), false); } - uint256 queueIndex = getUint("megapool.queue.index"); + uint256 queueIndex = getUint(keccak256("megapool.queue.index")); // TODO: Parameterise express_queue_rate uint256 expressQueueRate = 2; @@ -429,7 +449,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul uint256 expressQueueLength = linkedListStorage.getLength(expressQueueNamespace); uint256 standardQueueLength = linkedListStorage.getLength(standardQueueNamespace); - uint256 queueIndex = getUint("megapool.queue.index"); + uint256 queueIndex = getUint(keccak256("megapool.queue.index")); uint256 nodeBalanceUsed = 0; // TODO: Parameterise express_queue_rate @@ -482,8 +502,8 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul // Store state changes subUint("deposit.pool.node.balance", nodeBalanceUsed); - setUint("megapool.queue.index", queueIndex); - subUint("deposit.pool.requested.total", totalSent); + setUint(keccak256("megapool.queue.index"), queueIndex); + subUint(keccak256("deposit.pool.requested.total"), totalSent); } /// @dev Convenience method to return queue key for express and non-express queues diff --git a/contracts/contract/megapool/RocketMegapoolDelegate.sol b/contracts/contract/megapool/RocketMegapoolDelegate.sol index abc9ad4a..6864d3fc 100644 --- a/contracts/contract/megapool/RocketMegapoolDelegate.sol +++ b/contracts/contract/megapool/RocketMegapoolDelegate.sol @@ -14,7 +14,6 @@ import "../../interface/token/RocketTokenRETHInterface.sol"; import {RocketMegapoolProxy} from "./RocketMegapoolProxy.sol"; import "./RocketMegapoolDelegateBase.sol"; -import "hardhat/console.sol"; import {BeaconStateVerifier} from "../util/BeaconStateVerifier.sol"; import {RocketNetworkRevenuesInterface} from "../../interface/network/RocketNetworkRevenuesInterface.sol"; @@ -84,7 +83,7 @@ contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDel /// @notice Removes a validator from the deposit queue /// @param _validatorId the validator ID - function dequeue(uint32 _validatorId) external onlyLatestContract("rocketNodeDeposit", msg.sender) { + function dequeue(uint32 _validatorId) external onlyMegapoolOwner { ValidatorInfo memory validator = validators[_validatorId]; // Validate validator status require(validator.inQueue, "Validator must be in queue"); @@ -93,7 +92,6 @@ contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDel rocketDepositPool.exitQueue(_validatorId, validator.expressUsed); // Decrease total bond used for bond requirement calculations nodeBond -= validator.lastRequestedBond; - // TODO: Apply an ETH credit of validator.lastRequestedBond // Update validator state validator.inQueue = false; validator.lastRequestedBond = 0; @@ -338,22 +336,14 @@ contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDel function _calculateRewards(uint256 _rewards) internal view returns (uint256 nodeRewards, uint256 voterRewards, uint256 rethRewards) { RocketNetworkRevenuesInterface rocketNetworkRevenues = RocketNetworkRevenuesInterface(getContractAddress("rocketNetworkRevenues")); - console.log("lastDistributionBlock %d", lastDistributionBlock); (uint256 nodeShare, uint256 voterShare, uint256 rethShare) = rocketNetworkRevenues.calculateSplit(lastDistributionBlock); - console.log("%d %d %d", nodeShare, voterShare, rethShare); uint256 borrowedPortion = _rewards * userCapital / (nodeCapital + userCapital); - console.log("userCapital %d", userCapital); - console.log("nodeCapital %d", nodeCapital); - console.log("borrowedPortion %d", borrowedPortion); rethRewards = rethShare * borrowedPortion / calcBase; voterRewards = voterShare * borrowedPortion / calcBase; nodeRewards = _rewards - rethRewards - voterRewards; } function getPendingRewards() override public view returns (uint256) { - console.log("balance %d", address(this).balance); - console.log("refundValue %d", refundValue); - console.log("assignedValue %d", assignedValue); return address(this).balance - refundValue diff --git a/contracts/contract/node/RocketNodeDeposit.sol b/contracts/contract/node/RocketNodeDeposit.sol index 5f5580d2..96523970 100644 --- a/contracts/contract/node/RocketNodeDeposit.sol +++ b/contracts/contract/node/RocketNodeDeposit.sol @@ -103,7 +103,7 @@ contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { /// @notice Deposits ETH for the given node operator /// @param _nodeAddress The address of the node operator to deposit ETH for - function depositEthFor(address _nodeAddress) override external payable onlyRegisteredMinipool(_nodeAddress) { + function depositEthFor(address _nodeAddress) override external payable onlyLatestContract("rocketNodeDeposit", address(this)) onlyRegisteredNode(_nodeAddress) { // Send the ETH to vault uint256 amount = msg.value; RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); @@ -117,7 +117,7 @@ contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { /// @notice Withdraws ETH from a node operator's balance. Must be called from withdrawal address. /// @param _nodeAddress Address of the node operator to withdraw from /// @param _amount Amount of ETH to withdraw - function withdrawEth(address _nodeAddress, uint256 _amount) external onlyRegisteredMinipool(_nodeAddress) { + function withdrawEth(address _nodeAddress, uint256 _amount) external onlyLatestContract("rocketNodeDeposit", address(this)) onlyRegisteredNode(_nodeAddress) { // Check valid caller address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(_nodeAddress); require(msg.sender == withdrawalAddress, "Only withdrawal address can withdraw ETH"); @@ -135,7 +135,7 @@ contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { emit DepositFor(_nodeAddress, withdrawalAddress, _amount, block.timestamp); } - /// @notice Accept a node deposit and create a new minipool under the node. Only accepts calls from registered nodes + /// @notice Accept a node deposit and create a new validator under the node. Only accepts calls from registered nodes /// @param _bondAmount The amount of capital the node operator wants to put up as his bond /// @param _useExpressTicket If the express queue should be used /// @param _validatorPubkey Pubkey of the validator the node operator wishes to migrate diff --git a/contracts/contract/util/LinkedListStorage.sol b/contracts/contract/util/LinkedListStorage.sol index 0cb556a3..933bb27e 100644 --- a/contracts/contract/util/LinkedListStorage.sol +++ b/contracts/contract/util/LinkedListStorage.sol @@ -42,9 +42,9 @@ contract LinkedListStorage is RocketBase, LinkedListStorageInterface { /// @notice The index of an item in a queue. Returns 0 if the value is not found /// @param _namespace defines the queue to be used - /// @param _value the deposit queue value - function getIndexOf(bytes32 _namespace, DepositQueueValue memory _value) override external view returns (uint256) { - return getUint(keccak256(abi.encodePacked(_namespace, ".index", _value.receiver, _value.validatorId))); + /// @param _key the deposit queue value + function getIndexOf(bytes32 _namespace, DepositQueueKey memory _key) override external view returns (uint256) { + return getUint(keccak256(abi.encodePacked(_namespace, ".index", _key.receiver, _key.validatorId))); } /// @notice Finds an item index in a queue and returns the previous item @@ -158,16 +158,16 @@ contract LinkedListStorage is RocketBase, LinkedListStorageInterface { /// @notice Removes an item from a queue. Requires that the item exists in the queue /// @param _namespace defines the queue to be used - /// @param _item to be removed from the queue - function removeItem(bytes32 _namespace, DepositQueueValue memory _item) public virtual override onlyLatestContract("linkedListStorage", address(this)) onlyLatestNetworkContract { - _removeItem(_namespace, _item); + /// @param _key to be removed from the queue + function removeItem(bytes32 _namespace, DepositQueueKey memory _key) public virtual override onlyLatestContract("linkedListStorage", address(this)) onlyLatestNetworkContract { + _removeItem(_namespace, _key); } /// @notice Internal funciton to remove an item from a queue. Requires that the item exists in the queue /// @param _namespace defines the queue to be used - /// @param _item to be removed from the queue - function _removeItem(bytes32 _namespace, DepositQueueValue memory _item) internal { - uint256 index = getUint(keccak256(abi.encodePacked(_namespace, ".index", _item.receiver, _item.validatorId))); + /// @param _key to be removed from the queue + function _removeItem(bytes32 _namespace, DepositQueueKey memory _key) internal { + uint256 index = getUint(keccak256(abi.encodePacked(_namespace, ".index", _key.receiver, _key.validatorId))); uint256 data = getUint(keccak256(abi.encodePacked(_namespace, ".data"))); require(index > 0, "Item does not exist in queue"); @@ -194,7 +194,7 @@ contract LinkedListStorage is RocketBase, LinkedListStorageInterface { data |= prevIndex << endOffset; } - setUint(keccak256(abi.encodePacked(_namespace, ".index", _item.receiver, _item.validatorId)), 0); + setUint(keccak256(abi.encodePacked(_namespace, ".index", _key.receiver, _key.validatorId)), 0); setUint(keccak256(abi.encodePacked(_namespace, ".next", index)), 0); setUint(keccak256(abi.encodePacked(_namespace, ".prev", index)), 0); diff --git a/contracts/contract/util/LinkedListStorageHelper.sol b/contracts/contract/util/LinkedListStorageHelper.sol index 200677b5..bedd1ba8 100644 --- a/contracts/contract/util/LinkedListStorageHelper.sol +++ b/contracts/contract/util/LinkedListStorageHelper.sol @@ -28,9 +28,9 @@ contract LinkedListStorageHelper is LinkedListStorage { /// @notice Removes an item from a queue. Requires that the item exists in the queue /// @param _namespace to be used - /// @param _item to be removed from the queue - function removeItem(bytes32 _namespace, DepositQueueValue memory _item) public virtual override { - return _removeItem(_namespace, _item); + /// @param _key to be removed from the queue + function removeItem(bytes32 _namespace, DepositQueueKey memory _key) public virtual override { + return _removeItem(_namespace, _key); } function packItem(DepositQueueValue memory _item) public pure returns (uint256 packed) { diff --git a/contracts/interface/deposit/RocketDepositPoolInterface.sol b/contracts/interface/deposit/RocketDepositPoolInterface.sol index 23eb6cad..4a2bd1b8 100644 --- a/contracts/interface/deposit/RocketDepositPoolInterface.sol +++ b/contracts/interface/deposit/RocketDepositPoolInterface.sol @@ -6,11 +6,11 @@ interface RocketDepositPoolInterface { function getBalance() external view returns (uint256); function getNodeBalance() external view returns (uint256); function getUserBalance() external view returns (int256); + function getNodeCreditBalance(address _nodeAddress) external view returns (uint256); function getExcessBalance() external view returns (uint256); function deposit() external payable; function getMaximumDepositAmount() external view returns (uint256); function nodeDeposit(uint256 _totalAmount) external payable; - function nodeCreditWithdrawal(uint256 _amount) external; function recycleDissolvedDeposit() external payable; function recycleExcessCollateral() external payable; function recycleLiquidatedStake() external payable; @@ -18,8 +18,9 @@ interface RocketDepositPoolInterface { function maybeAssignOneDeposit() external; function maybeAssignDeposits() external returns (bool); function withdrawExcessBalance(uint256 _amount) external; - function requestFunds(uint256 _bondAmount, uint256 _validatorIndex, uint256 _amount, bool _useExpressTicket) external payable; + function requestFunds(uint256 _bondAmount, uint256 _validatorIndex, uint256 _amount, bool _useExpressTicket) external; function exitQueue(uint256 validatorIndex, bool expressQueue) external; + function withdrawCredit(uint256 _amount) external; function getQueueTop() external view returns (address receiver, bool assignmentPossible); function assignMegapools(uint256 _count) external; } diff --git a/contracts/interface/util/LinkedListStorageInterface.sol b/contracts/interface/util/LinkedListStorageInterface.sol index dd18925f..3c5a3caf 100644 --- a/contracts/interface/util/LinkedListStorageInterface.sol +++ b/contracts/interface/util/LinkedListStorageInterface.sol @@ -10,12 +10,17 @@ struct DepositQueueValue { uint32 requestedValue; // in milliether } +struct DepositQueueKey { + address receiver; // the address that will receive the requested value + uint32 validatorId; // internal validator id +} + interface LinkedListStorageInterface { function getLength(bytes32 _namespace) external view returns (uint256); function getItem(bytes32 _namespace, uint _index) external view returns (DepositQueueValue memory); function peekItem(bytes32 _namespace) external view returns (DepositQueueValue memory); - function getIndexOf(bytes32 _namespace, DepositQueueValue memory _value) external view returns (uint256); + function getIndexOf(bytes32 _namespace, DepositQueueKey memory _key) external view returns (uint256); function enqueueItem(bytes32 _namespace, DepositQueueValue memory _value) external; function dequeueItem(bytes32 _namespace) external returns (DepositQueueValue memory); - function removeItem(bytes32 _namespace, DepositQueueValue memory _value) external; + function removeItem(bytes32 _namespace, DepositQueueKey memory _key) external; } diff --git a/test/megapool/megapool-tests.js b/test/megapool/megapool-tests.js index 33b96503..363b6243 100644 --- a/test/megapool/megapool-tests.js +++ b/test/megapool/megapool-tests.js @@ -8,7 +8,7 @@ import { shouldRevert } from '../_utils/testing'; import { BeaconStateVerifier, MegapoolUpgradeHelper, - RocketDAONodeTrustedSettingsMinipool, + RocketDAONodeTrustedSettingsMinipool, RocketDepositPool, RocketMegapoolDelegate, RocketMegapoolFactory, RocketStorage, @@ -17,6 +17,7 @@ import assert from 'assert'; import { setDAONodeTrustedBootstrapSetting } from '../dao/scenario-dao-node-trusted-bootstrap'; import { stakeMegapoolValidator } from './scenario-stake'; import { assertBN } from '../_helpers/bn'; +import { exitQueue } from './scenario-exit-queue'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); @@ -84,6 +85,12 @@ export default function() { await shouldRevert(deployMegapool({ from: node }), 'Redeploy worked'); }); + it(printTitle('node', 'can exit the deposit queue'), async () => { + await deployMegapool({ from: node }); + await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await exitQueue(node, 0); + }); + describe('With full deposit pool', () => { const dissolvePeriod = (60 * 60 * 24); // 24 hours @@ -94,6 +101,12 @@ export default function() { await setDAONodeTrustedBootstrapSetting(RocketDAONodeTrustedSettingsMinipool, 'megapool.dissolve.period', dissolvePeriod, { from: owner }); }); + it(printTitle('node', 'cannot exit the deposit queue once assigned'), async () => { + await deployMegapool({ from: node }); + await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await shouldRevert(exitQueue(node, 0), 'Was able to exit the deposit queue once assigned', 'Validator must be in queue'); + }); + it(printTitle('node', 'can not create a new validator while debt is present'), async () => { await deployMegapool({ from: node }); await megapool.connect(owner).setDebt('1'.ether); @@ -200,7 +213,6 @@ export default function() { rETH Share: 1 - 0.875 - 0.04375 = 0.08125 ETH */ const rewardSplit = await megapool.calculateRewards(); - console.log(rewardSplit); assertBN.equal(rewardSplit[0], '0.16875'.ether); assertBN.equal(rewardSplit[1], '0.07875'.ether); assertBN.equal(rewardSplit[2], '0.7525'.ether); diff --git a/test/megapool/scenario-exit-queue.js b/test/megapool/scenario-exit-queue.js new file mode 100644 index 00000000..ed7c3212 --- /dev/null +++ b/test/megapool/scenario-exit-queue.js @@ -0,0 +1,25 @@ +import { getMegapoolForNode } from '../_helpers/megapool'; +import assert from 'assert'; +import { RocketDepositPool } from '../_utils/artifacts'; +import { assertBN } from '../_helpers/bn'; + +const milliToWei = 1000000000000000n; + +export async function exitQueue(node, validatorIndex) { + const megapool = await getMegapoolForNode(node); + const rocketDepositPool = await RocketDepositPool.deployed(); + + const validatorInfoBefore = await megapool.getValidatorInfo(validatorIndex); + + // Dequeue the validator + await megapool.dequeue(validatorIndex); + + // Check the validator status + const validatorInfoAfter = await megapool.getValidatorInfo(validatorIndex); + assert.equal(validatorInfoAfter.active, false); + assert.equal(validatorInfoAfter.inQueue, false); + + // Check an ETH credit was applied + const credit = await rocketDepositPool.getNodeCreditBalance(node.address); + assertBN.equal(credit, validatorInfoBefore.lastRequestedBond * milliToWei); +} \ No newline at end of file