diff --git a/contracts/contract/deposit/RocketDepositPool.sol b/contracts/contract/deposit/RocketDepositPool.sol index 1c0d5394..585f5e6f 100644 --- a/contracts/contract/deposit/RocketDepositPool.sol +++ b/contracts/contract/deposit/RocketDepositPool.sol @@ -81,7 +81,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul /// @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))); + return getUint(keccak256(abi.encodePacked("node.deposit.credit.balance", _nodeAddress))); } /// @notice Excess deposit pool balance (in excess of minipool queue capacity) @@ -379,7 +379,7 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul // 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); + addUint(keccak256(abi.encodePacked("node.deposit.credit.balance", nodeAddress)), value.suppliedValue * milliToWei); if (_expressQueue) { // Refund express ticket RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress("rocketNodeManager")); @@ -401,10 +401,10 @@ contract RocketDepositPool is RocketBase, RocketDepositPoolInterface, RocketVaul /// @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))); + uint256 credit = getUint(keccak256(abi.encodePacked("node.deposit.credit.balance", 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(keccak256(abi.encodePacked("node.deposit.credit.balance", 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? diff --git a/contracts/contract/megapool/RocketMegapoolDelegate.sol b/contracts/contract/megapool/RocketMegapoolDelegate.sol index 38307acb..7789a125 100644 --- a/contracts/contract/megapool/RocketMegapoolDelegate.sol +++ b/contracts/contract/megapool/RocketMegapoolDelegate.sol @@ -16,7 +16,6 @@ import {IERC20} from "../../interface/util/IERC20.sol"; import {RocketMegapoolDelegateBase} from "./RocketMegapoolDelegateBase.sol"; import {RocketMegapoolStorageLayout} from "./RocketMegapoolStorageLayout.sol"; -/// @title RocketMegapool /// @notice This contract manages multiple validators. It serves as the target of Beacon Chain withdrawal credentials. contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDelegateInterface { // Constants @@ -95,7 +94,9 @@ contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDel RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); rocketDepositPool.exitQueue(_validatorId, validator.expressUsed); // Decrease total bond used for bond requirement calculations - nodeBond -= validator.lastRequestedBond; + nodeBond -= validator.lastRequestedBond * milliToWei; + // Increment inactive validator count + numInactiveValidators++; // Update validator state validator.inQueue = false; validator.lastRequestedBond = 0; @@ -329,6 +330,11 @@ contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDel return numValidators; } + /// @notice Returns the number of validators that are considered for bond requirement + function getActiveValidatorCount() override external view returns (uint32) { + return numValidators - numInactiveValidators; + } + /// @notice Returns information about a given validator function getValidatorInfo(uint32 _validatorId) override external view returns (RocketMegapoolStorageLayout.ValidatorInfo memory) { return validators[_validatorId]; @@ -366,13 +372,13 @@ contract RocketMegapoolDelegate is RocketMegapoolDelegateBase, RocketMegapoolDel return _calculateRewards(_amount); } - function _calculateRewards(uint256 _rewards) internal view returns (uint256 nodeAmount, uint256 voterAmount, uint256 rethAmount) { + function _calculateRewards(uint256 _rewards) internal view returns (uint256 nodeRewards, uint256 voterRewards, uint256 rethRewards) { RocketNetworkRevenuesInterface rocketNetworkRevenues = RocketNetworkRevenuesInterface(getContractAddress("rocketNetworkRevenues")); (, uint256 voterShare, uint256 rethShare) = rocketNetworkRevenues.calculateSplit(lastDistributionBlock); uint256 borrowedPortion = _rewards * userCapital / (nodeCapital + userCapital); - rethAmount = rethShare * borrowedPortion / calcBase; - voterAmount = voterShare * borrowedPortion / calcBase; - nodeAmount = _rewards - rethAmount - voterAmount; + rethRewards = rethShare * borrowedPortion / calcBase; + voterRewards = voterShare * borrowedPortion / calcBase; + nodeRewards = _rewards - rethRewards - voterRewards; } function getPendingRewards() override public view returns (uint256) { diff --git a/contracts/contract/megapool/RocketMegapoolManager.sol b/contracts/contract/megapool/RocketMegapoolManager.sol index 934091ad..6583c83a 100644 --- a/contracts/contract/megapool/RocketMegapoolManager.sol +++ b/contracts/contract/megapool/RocketMegapoolManager.sol @@ -32,12 +32,12 @@ contract RocketMegapoolManager is RocketBase, RocketMegapoolManagerInterface { /// @notice Returns validator info for the given global megapool validator index /// @param _index The index of the validator to query - function getValidatorInfo(uint256 _index) override external view returns (RocketMegapoolStorageLayout.ValidatorInfo memory, address, uint32) { + function getValidatorInfo(uint256 _index) override external view returns (RocketMegapoolStorageLayout.ValidatorInfo memory validatorInfo, address megapool, uint32 validatorId) { uint256 encoded = getUint(keccak256(abi.encodePacked("megapool.validator.set", _index))); - address megapoolAddress = address(uint160(encoded >> 96)); - uint32 validatorId = uint32(encoded); + megapool = address(uint160(encoded >> 96)); + validatorId = uint32(encoded); - RocketMegapoolInterface rocketMegapool = RocketMegapoolInterface(megapoolAddress); - return (rocketMegapool.getValidatorInfo(validatorId), megapoolAddress, validatorId); + RocketMegapoolInterface rocketMegapool = RocketMegapoolInterface(megapool); + return (rocketMegapool.getValidatorInfo(validatorId), megapool, validatorId); } } diff --git a/contracts/contract/megapool/RocketMegapoolStorageLayout.sol b/contracts/contract/megapool/RocketMegapoolStorageLayout.sol index bfa922ce..3c3e5524 100644 --- a/contracts/contract/megapool/RocketMegapoolStorageLayout.sol +++ b/contracts/contract/megapool/RocketMegapoolStorageLayout.sol @@ -77,9 +77,5 @@ abstract contract RocketMegapoolStorageLayout { mapping(uint32 => ValidatorInfo) internal validators; mapping(uint32 => PrestakeData) internal prestakeData; - // TODO: Move this to rocketNodeStaking - uint256 internal stakedRPL; - uint256 internal unstakedRPL; - uint256 internal lastUnstakeRequest; - + uint32 internal numInactiveValidators; // Number of validators that are no longer contributing to bond requirement } diff --git a/contracts/contract/network/RocketNetworkRevenues.sol b/contracts/contract/network/RocketNetworkRevenues.sol index 5edca413..22056678 100644 --- a/contracts/contract/network/RocketNetworkRevenues.sol +++ b/contracts/contract/network/RocketNetworkRevenues.sol @@ -80,10 +80,8 @@ contract RocketNetworkRevenues is RocketBase, RocketNetworkRevenuesInterface { /// @notice Calculates the time-weighted average revenue split values between the supplied block number and now /// @param _sinceBlock The starting block number for the calculation - function calculateSplit(uint256 _sinceBlock) external override view returns (uint256, uint256, uint256) { + function calculateSplit(uint256 _sinceBlock) external override view returns (uint256 nodeShare, uint256 voterShare, uint256 rethShare) { RocketNetworkSnapshotsInterface rocketNetworkSnapshots = RocketNetworkSnapshotsInterface(getContractAddress("rocketNetworkSnapshots")); - uint256 nodeShare; - uint256 voterShare; if (_sinceBlock == block.number) { nodeShare = _getCurrentShare(rocketNetworkSnapshots, nodeShareKey); voterShare = _getCurrentShare(rocketNetworkSnapshots, voterShareKey); @@ -103,7 +101,7 @@ contract RocketNetworkRevenues is RocketBase, RocketNetworkRevenuesInterface { voterShare *= shareScale; } uint256 rethCommission = nodeShare + voterShare; - uint256 rethShare = 1 ether - rethCommission; + rethShare = 1 ether - rethCommission; return (nodeShare, voterShare, rethShare); } diff --git a/contracts/contract/node/RocketNodeDeposit.sol b/contracts/contract/node/RocketNodeDeposit.sol index a9e12af9..3321f32c 100644 --- a/contracts/contract/node/RocketNodeDeposit.sol +++ b/contracts/contract/node/RocketNodeDeposit.sol @@ -16,9 +16,10 @@ import "../../interface/node/RocketNodeManagerInterface.sol"; import "../../interface/node/RocketNodeStakingInterface.sol"; import "../RocketBase.sol"; import {RocketMegapoolManagerInterface} from "../../interface/megapool/RocketMegapoolManagerInterface.sol"; +import {RocketVaultWithdrawerInterface} from "../../interface/RocketVaultWithdrawerInterface.sol"; /// @notice Entry point for node operators to perform deposits for the creation of new validators on the network -contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { +contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface, RocketVaultWithdrawerInterface { // Constants uint256 constant internal pubKeyLength = 48; uint256 constant internal signatureLength = 96; @@ -32,9 +33,12 @@ contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { version = 5; } - /// @dev Accept incoming ETH from the deposit pool + /// @notice Accept incoming ETH from the deposit pool receive() external payable onlyLatestContract("rocketDepositPool", msg.sender) {} + /// @notice Accept incoming ETH from the vault + function receiveVaultWithdrawalETH() external payable {} + /// @notice Returns the bond requirement for the given number of validators function getBondRequirement(uint256 _numValidators) override public view returns (uint256) { // Get contracts @@ -133,7 +137,7 @@ contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { // Check amount require(msg.value == _bondAmount, "Invalid value"); // Process the deposit - _deposit(_bondAmount, _useExpressTicket, _validatorPubkey, _validatorSignature, _depositDataRoot); + _deposit(_bondAmount, _useExpressTicket, _validatorPubkey, _validatorSignature, _depositDataRoot, msg.value); } /// @notice Accept a node deposit and create a new minipool under the node. Only accepts calls from registered nodes @@ -143,38 +147,35 @@ contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { /// @param _validatorSignature Signature from the validator over the deposit data /// @param _depositDataRoot The hash tree root of the deposit data (passed onto the deposit contract on pre stake) function depositWithCredit(uint256 _bondAmount, bool _useExpressTicket, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) override external payable onlyLatestContract("rocketNodeDeposit", address(this)) onlyRegisteredNode(msg.sender) { - revert("Not implemented"); -// { -// uint256 balanceToUse = 0; -// uint256 creditToUse = 0; -// uint256 shortFall = _bondAmount - msg.value; -// uint256 credit = getNodeUsableCredit(msg.sender); -// uint256 balance = getNodeEthBalance(msg.sender); -// // Check credit -// require(credit + balance >= shortFall, "Insufficient credit"); -// // Calculate amounts to use -// creditToUse = shortFall; -// if (credit < shortFall) { -// balanceToUse = shortFall - credit; -// creditToUse = credit; -// } -// // Update balances -// if (balanceToUse > 0) { -// subUint(keccak256(abi.encodePacked("node.eth.balance", msg.sender)), balanceToUse); -// // Withdraw the funds -// RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); -// rocketVault.withdrawEther(balanceToUse); -// } -// if (creditToUse > 0) { -// subUint(keccak256(abi.encodePacked("node.deposit.credit.balance", msg.sender)), creditToUse); -// } -// } -// // Process the deposit -// _deposit(_bondAmount, _useExpressTicket, _validatorPubkey, _validatorSignature, _depositDataRoot); + uint256 balanceToUse = 0; + uint256 creditToUse = 0; + uint256 shortFall = _bondAmount - msg.value; + uint256 credit = getNodeUsableCredit(msg.sender); + uint256 balance = getNodeEthBalance(msg.sender); + // Check credit + require(credit + balance >= shortFall, "Insufficient credit"); + // Calculate amounts to use + creditToUse = shortFall; + if (credit < shortFall) { + balanceToUse = shortFall - credit; + creditToUse = credit; + } + // Update balances + if (balanceToUse > 0) { + subUint(keccak256(abi.encodePacked("node.eth.balance", msg.sender)), balanceToUse); + // Withdraw the funds + RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault")); + rocketVault.withdrawEther(balanceToUse); + } + if (creditToUse > 0) { + subUint(keccak256(abi.encodePacked("node.deposit.credit.balance", msg.sender)), creditToUse); + } + // Process the deposit + _deposit(_bondAmount, _useExpressTicket, _validatorPubkey, _validatorSignature, _depositDataRoot, msg.value + balanceToUse); } /// @dev Internal logic to process a deposit - function _deposit(uint256 _bondAmount, bool _useExpressTicket, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) private { + function _deposit(uint256 _bondAmount, bool _useExpressTicket, bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot, uint256 _value) private { // Validate arguments validateBytes(_validatorPubkey, pubKeyLength); validateBytes(_validatorSignature, signatureLength); @@ -182,7 +183,7 @@ contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { checkVotingInitialised(); checkDepositsEnabled(); // Emit deposit received event - emit DepositReceived(msg.sender, msg.value, block.timestamp); + emit DepositReceived(msg.sender, _value, block.timestamp); // Get or deploy a megapool for the caller RocketMegapoolFactoryInterface rocketMegapoolFactory = RocketMegapoolFactoryInterface(getContractAddress("rocketMegapoolFactory")); RocketMegapoolInterface megapool = RocketMegapoolInterface(rocketMegapoolFactory.getOrDeployContract(msg.sender)); @@ -195,7 +196,7 @@ contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { rocketMegapoolManager.addValidator(address(megapool), megapool.getValidatorCount()); // Send node operator's bond to the deposit pool RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress("rocketDepositPool")); - rocketDepositPool.nodeDeposit{value: msg.value}(_bondAmount); + rocketDepositPool.nodeDeposit{value: _value}(_bondAmount); // Attempt to assign 1 megapool rocketDepositPool.maybeAssignOneDeposit(); } @@ -230,7 +231,7 @@ contract RocketNodeDeposit is RocketBase, RocketNodeDepositInterface { /// @dev Checks the bond requirements for a node deposit function checkBondRequirement(RocketMegapoolInterface _megapool, uint256 _bondAmount) internal { - uint256 totalBondRequired = getBondRequirement(_megapool.getValidatorCount()); + uint256 totalBondRequired = getBondRequirement(_megapool.getActiveValidatorCount()); uint256 currentBond = _megapool.getNodeBond(); uint256 requiredBond = totalBondRequired - currentBond; require(_bondAmount == requiredBond, "Bond requirement not met"); diff --git a/contracts/contract/util/LinkedListStorage.sol b/contracts/contract/util/LinkedListStorage.sol index 08ee42e3..4a259807 100644 --- a/contracts/contract/util/LinkedListStorage.sol +++ b/contracts/contract/util/LinkedListStorage.sol @@ -233,9 +233,9 @@ contract LinkedListStorage is RocketBase, LinkedListStorageInterface { } /// @notice Returns the supplied number of entries starting at the supplied index - function scan(bytes32 _namespace, uint256 _start, uint256 _count) override external view returns (DepositQueueValue[] memory, uint256) { - DepositQueueValue[] memory entries = new DepositQueueValue[](_count); - uint256 nextIndex = _start; + function scan(bytes32 _namespace, uint256 _start, uint256 _count) override external view returns (DepositQueueValue[] memory entries, uint256 nextIndex) { + entries = new DepositQueueValue[](_count); + nextIndex = _start; uint256 total = 0; // If nextIndex is 0, begin scan at the start of the list diff --git a/contracts/interface/megapool/RocketMegapoolDelegateInterface.sol b/contracts/interface/megapool/RocketMegapoolDelegateInterface.sol index 2ad1c4fb..dbd2fe3c 100644 --- a/contracts/interface/megapool/RocketMegapoolDelegateInterface.sol +++ b/contracts/interface/megapool/RocketMegapoolDelegateInterface.sol @@ -21,6 +21,7 @@ interface RocketMegapoolDelegateInterface is RocketMegapoolDelegateBaseInterface function repayDebt() external payable; function getValidatorCount() external view returns (uint32); + function getActiveValidatorCount() external view returns (uint32); function getValidatorInfo(uint32 _validatorId) external view returns (RocketMegapoolStorageLayout.ValidatorInfo memory); function getAssignedValue() external view returns (uint256); function getDebt() external view returns (uint256); diff --git a/contracts/interface/megapool/RocketMegapoolManagerInterface.sol b/contracts/interface/megapool/RocketMegapoolManagerInterface.sol index 82f1d8fd..56526d4a 100644 --- a/contracts/interface/megapool/RocketMegapoolManagerInterface.sol +++ b/contracts/interface/megapool/RocketMegapoolManagerInterface.sol @@ -6,5 +6,5 @@ import "../../contract/megapool/RocketMegapoolStorageLayout.sol"; interface RocketMegapoolManagerInterface { function getValidatorCount() external view returns (uint256); function addValidator(address _megapoolAddress, uint32 _validatorId) external; - function getValidatorInfo(uint256 _index) external view returns (RocketMegapoolStorageLayout.ValidatorInfo memory validatorInfo, address megapoolAddress, uint32 validatorId); + function getValidatorInfo(uint256 _index) external view returns (RocketMegapoolStorageLayout.ValidatorInfo memory validatorInfo, address megapool, uint32 validatorId); } \ No newline at end of file diff --git a/contracts/interface/network/RocketNetworkRevenuesInterface.sol b/contracts/interface/network/RocketNetworkRevenuesInterface.sol index d66bcf3d..f4007472 100644 --- a/contracts/interface/network/RocketNetworkRevenuesInterface.sol +++ b/contracts/interface/network/RocketNetworkRevenuesInterface.sol @@ -7,5 +7,5 @@ interface RocketNetworkRevenuesInterface { function getCurrentVoterShare() external view returns (uint256); function setNodeShare(uint256 _newShare) external; function setVoterShare(uint256 _newShare) external; - function calculateSplit(uint256 _sinceBlock) external view returns (uint256, uint256, uint256); + function calculateSplit(uint256 _sinceBlock) external view returns (uint256 nodeShare, uint256 voterShare, uint256 rethShare); } diff --git a/contracts/interface/util/LinkedListStorageInterface.sol b/contracts/interface/util/LinkedListStorageInterface.sol index 0897983b..126bcfd3 100644 --- a/contracts/interface/util/LinkedListStorageInterface.sol +++ b/contracts/interface/util/LinkedListStorageInterface.sol @@ -24,5 +24,5 @@ interface LinkedListStorageInterface { function enqueueItem(bytes32 _namespace, DepositQueueValue memory _value) external; function dequeueItem(bytes32 _namespace) external returns (DepositQueueValue memory); function removeItem(bytes32 _namespace, DepositQueueKey memory _key) external; - function scan(bytes32 _namespace, uint256 _startIndex, uint256 _count) external view returns (DepositQueueValue[] memory, uint256 nextIndex); + function scan(bytes32 _namespace, uint256 _startIndex, uint256 _count) external view returns (DepositQueueValue[] memory entries, uint256 nextIndex); } diff --git a/test-upgrade/rocket-upgrade-tests.js b/test-upgrade/rocket-upgrade-tests.js index 3577d597..4b971855 100644 --- a/test-upgrade/rocket-upgrade-tests.js +++ b/test-upgrade/rocket-upgrade-tests.js @@ -97,7 +97,7 @@ describe('Test Upgrade', () => { await executeUpgrade(); await deployMegapool({ from: node }); - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); const megapool = await getMegapoolForNode(node); diff --git a/test/_helpers/megapool.js b/test/_helpers/megapool.js index 88573ffd..c955a019 100644 --- a/test/_helpers/megapool.js +++ b/test/_helpers/megapool.js @@ -44,7 +44,7 @@ export async function deployMegapool(txOptions) { assert.equal(existsAfter, true, "Megapool was not created"); } -export async function nodeDeposit(useExpressTicket, useCredit, txOptions) { +export async function nodeDeposit(node, bondAmount = '4'.ether, useExpressTicket = false, creditAmount = '0'.ether) { const [ rocketNodeDeposit, rocketNodeManager, @@ -62,7 +62,7 @@ export async function nodeDeposit(useExpressTicket, useCredit, txOptions) { ]); // Construct deposit data for prestake - let withdrawalCredentials = await getMegapoolWithdrawalCredentials(txOptions.from.address); + let withdrawalCredentials = await getMegapoolWithdrawalCredentials(node.address); let depositData = { pubkey: getValidatorPubkey(), withdrawalCredentials: Buffer.from(withdrawalCredentials.substr(2), 'hex'), @@ -70,11 +70,12 @@ export async function nodeDeposit(useExpressTicket, useCredit, txOptions) { signature: getValidatorSignature(), }; let depositDataRoot = getDepositDataRoot(depositData); + let usingCredit = creditAmount > 0n; async function getData() { let data = await Promise.all([ - rocketMegapoolFactory.getMegapoolDeployed(txOptions.from.address), - rocketNodeManager.getExpressTicketCount(txOptions.from.address), + rocketMegapoolFactory.getMegapoolDeployed(node.address), + rocketNodeManager.getExpressTicketCount(node.address), rocketMegapoolManager.getValidatorCount(), ]).then( ([ deployed, numExpressTickets, numGlobalValidators]) => @@ -82,7 +83,7 @@ export async function nodeDeposit(useExpressTicket, useCredit, txOptions) { ); if (data.deployed) { - const megapool = (await getMegapoolForNode(txOptions.from)); + const megapool = (await getMegapoolForNode(node)); data.numValidators = await megapool.getValidatorCount(); data.assignedValue = await megapool.getAssignedValue(); data.nodeCapital = await megapool.getNodeCapital(); @@ -96,11 +97,20 @@ export async function nodeDeposit(useExpressTicket, useCredit, txOptions) { const assignmentsEnabled = await rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled(); const depositPoolCapacity = await rocketDepositPool.getBalance(); - const amountRequired = '32'.ether - txOptions.value; + const amountRequired = '32'.ether - bondAmount; const expectAssignment = assignmentsEnabled && depositPoolCapacity >= amountRequired; - const tx = await rocketNodeDeposit.connect(txOptions.from).deposit(txOptions.value, useExpressTicket, depositData.pubkey, depositData.signature, depositDataRoot, txOptions); - await tx.wait(); + if (!usingCredit) { + const tx = await rocketNodeDeposit.connect(node).deposit(bondAmount, useExpressTicket, depositData.pubkey, depositData.signature, depositDataRoot, {value: bondAmount}); + await tx.wait(); + } else { + const creditBefore = await rocketNodeDeposit.getNodeUsableCreditAndBalance(node.address); + const tx = await rocketNodeDeposit.connect(node).depositWithCredit(bondAmount, useExpressTicket, depositData.pubkey, depositData.signature, depositDataRoot, {value: bondAmount - creditAmount}); + await tx.wait(); + const creditAfter = await rocketNodeDeposit.getNodeUsableCreditAndBalance(node.address); + const creditDelta = creditAfter - creditBefore; + assertBN.equal(creditDelta, -creditAmount); + } const data2 = await getData(); @@ -126,7 +136,7 @@ export async function nodeDeposit(useExpressTicket, useCredit, txOptions) { } // Confirm state of new validator - const megapool = await getMegapoolForNode(txOptions.from); + const megapool = await getMegapoolForNode(node); const validatorInfo = await getValidatorInfo(megapool, data1.numValidators); if (expectAssignment) { @@ -144,7 +154,7 @@ export async function nodeDeposit(useExpressTicket, useCredit, txOptions) { } assertBN.equal(validatorInfo.lastRequestedValue, '32'.ether / milliToWei, "Incorrect validator lastRequestedValue"); - assertBN.equal(validatorInfo.lastRequestedBond, txOptions.value / milliToWei, "Incorrect validator lastRequestedBond"); + assertBN.equal(validatorInfo.lastRequestedBond, bondAmount / milliToWei, "Incorrect validator lastRequestedBond"); assert.equal(validatorInfo.staked, false, "Incorrect validator status"); assert.equal(validatorInfo.dissolved, false, "Incorrect validator status"); diff --git a/test/dao/scenario-dao-node-trusted-bootstrap.js b/test/dao/scenario-dao-node-trusted-bootstrap.js index 65af0b5e..e819936a 100644 --- a/test/dao/scenario-dao-node-trusted-bootstrap.js +++ b/test/dao/scenario-dao-node-trusted-bootstrap.js @@ -137,7 +137,7 @@ export async function setDaoNodeTrustedBootstrapUpgrade(_type, _name, _abi, _con let contract1 = await getContractData(); // Upgrade contract - await rocketDAONodeTrusted.connect(txOptions.from).bootstrapUpgrade(_type, _name, compressedAbi, _contractAddress, txOptions); + await (await rocketDAONodeTrusted.connect(txOptions.from).bootstrapUpgrade(_type, _name, compressedAbi, _contractAddress, txOptions)).wait(); // Get updated contract data let contract2 = await getContractData(); diff --git a/test/deposit/deposit-pool-tests.js b/test/deposit/deposit-pool-tests.js index a6304f67..b3acb3d6 100644 --- a/test/deposit/deposit-pool-tests.js +++ b/test/deposit/deposit-pool-tests.js @@ -115,7 +115,7 @@ export default function() { }), 'Made a deposit which exceeds the maximum deposit pool size'); // Perform 4 node deposits so there is 16 ETH space available for user deposits for (let i = 0; i < 4; ++i) { - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); } // Attempt deposit await deposit({ @@ -143,7 +143,7 @@ export default function() { // Deposit and queue up some validators await userDeposit({ from: staker, value: '100'.ether }); for (let i = 0; i < 3; ++i) { - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); } // Re-enable deposit assignment & set limit await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', true, { from: owner }); @@ -183,7 +183,7 @@ export default function() { await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', false, { from: owner }); // Create 4 validators totally 112 ETH extra capacity for (let i = 0; i < 4; ++i) { - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); } // Enable assignments to make that extra capacity usable await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', true, { from: owner }); diff --git a/test/megapool/megapool-tests.js b/test/megapool/megapool-tests.js index 9c6b00ea..425162fb 100644 --- a/test/megapool/megapool-tests.js +++ b/test/megapool/megapool-tests.js @@ -1,6 +1,6 @@ import { before, describe, it } from 'mocha'; import { printTitle } from '../_utils/formatting'; -import { registerNode, setNodeWithdrawalAddress } from '../_helpers/node'; +import { nodeDepositEthFor, registerNode, setNodeWithdrawalAddress } from '../_helpers/node'; import { globalSnapShot } from '../_utils/snapshotting'; import { userDeposit } from '../_helpers/deposit'; import { @@ -13,22 +13,21 @@ import { import { shouldRevert } from '../_utils/testing'; import { BeaconStateVerifier, - LinkedListStorage, MegapoolUpgradeHelper, - RocketDAONodeTrustedSettingsMinipool, - RocketDAOProtocolSettingsDeposit, RocketDAOProtocolSettingsMegapool, + RocketDAOProtocolSettingsDeposit, + RocketDAOProtocolSettingsMegapool, RocketDepositPool, RocketMegapoolDelegate, RocketMegapoolFactory, RocketStorage, } from '../_utils/artifacts'; 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'; import { setDAOProtocolBootstrapSetting } from '../dao/scenario-dao-protocol-bootstrap'; import { distributeMegapool } from './scenario-distribute'; +import { withdrawCredit } from './scenario-withdraw-credit'; const helpers = require('@nomicfoundation/hardhat-network-helpers'); const hre = require('hardhat'); @@ -91,7 +90,20 @@ export default function() { it(printTitle('node', 'can manually deploy a megapool then deposit'), async () => { await deployMegapool({ from: node }); - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); + }); + + it(printTitle('node', 'can deposit using supplied ETH'), async () => { + await nodeDepositEthFor(node, { from: random, value: '4'.ether }); + await nodeDeposit(node, '4'.ether, false, '4'.ether); + }); + + it(printTitle('node', 'can deposit using ETH credit'), async () => { + // Enter and exit queue to receive a 4 ETH credit + await nodeDeposit(node, '4'.ether); + await exitQueue(node, 0); + // Use credit on entering the queue again + await nodeDeposit(node, '4'.ether, false, '4'.ether); }); it(printTitle('node', 'can not deploy megapool twice'), async () => { @@ -99,10 +111,14 @@ export default function() { await shouldRevert(deployMegapool({ from: node }), 'Redeploy worked'); }); - it(printTitle('node', 'can exit the deposit queue'), async () => { + it(printTitle('node', 'can exit the deposit queue and withdraw credit as rETH'), async () => { await deployMegapool({ from: node }); - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); await exitQueue(node, 0); + // Withdraw 1 ETH worth of rETH + await withdrawCredit(node, '1'.ether); + // Fail to withdraw 4 ETH worth of rETH + await shouldRevert(withdrawCredit(node, '4'.ether), 'Withdrew more rETH than credit', 'Amount exceeds credit available'); }); it(printTitle('node', 'can queue up and exit multiple validators'), async () => { @@ -113,7 +129,7 @@ export default function() { await deployMegapool({ from: node }); for (let i = 0; i < numValidators; i++) { - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); const position = await calculatePositionInQueue(megapool, i); assertBN.equal(position, BigInt(i)); @@ -134,7 +150,6 @@ export default function() { } }); - it(printTitle('misc', 'calculates position in queue correctly'), async () => { const rocketDepositPool = await RocketDepositPool.deployed(); @@ -150,11 +165,11 @@ export default function() { * 4: node-4 */ - await nodeDeposit(false, false, { value: '4'.ether, from: node }); // 0 - await nodeDeposit(true, false, { value: '4'.ether, from: node }); // 1 (express) - await nodeDeposit(true, false, { value: '4'.ether, from: node }); // 2 (express) - await nodeDeposit(false, false, { value: '4'.ether, from: node }); // 3 - await nodeDeposit(false, false, { value: '4'.ether, from: node }); // 4 + await nodeDeposit(node); // 0 + await nodeDeposit(node, '4'.ether, true); // 1 (express) + await nodeDeposit(node, '4'.ether, true); // 2 (express) + await nodeDeposit(node); // 3 + await nodeDeposit(node); // 4 assertBN.equal(await calculatePositionInQueue(megapool, 1n), 0n); assertBN.equal(await calculatePositionInQueue(megapool, 2n), 1n); @@ -190,8 +205,8 @@ export default function() { await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', false, { from: owner }); // Add 2 more validators in the express queue - await nodeDeposit(true, false, { value: '4'.ether, from: node2 }); - await nodeDeposit(true, false, { value: '4'.ether, from: node2 }); + await nodeDeposit(node2, '4'.ether, true); + await nodeDeposit(node2, '4'.ether, true); const megapool2 = await getMegapoolForNode(node2); /** @@ -252,7 +267,7 @@ export default function() { await setDAOProtocolBootstrapSetting(RocketDAOProtocolSettingsDeposit, 'deposit.assign.enabled', false, { from: owner }); // Deploy a validator await deployMegapool({ from: node }); - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); const topBefore = await rocketDepositPool.getQueueTop(); assert.equal(topBefore[1], false); // Check the validator is still in the queue @@ -273,75 +288,54 @@ export default function() { 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 nodeDeposit(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); - await shouldRevert(nodeDeposit(false, false, { - value: '4'.ether, - from: node, - }), 'Created validator', 'Cannot create validator while debt exists'); + await shouldRevert(nodeDeposit(node), 'Created validator', 'Cannot create validator while debt exists'); }); it(printTitle('node', 'can create new validators per bond requirements'), async () => { - await shouldRevert(nodeDeposit(false, false, { - value: '8'.ether, - from: node, - }), 'Created validator', 'Bond requirement not met'); - await shouldRevert(nodeDeposit(false, false, { - value: '2'.ether, - from: node, - }), 'Created validator', 'Bond requirement not met'); - await nodeDeposit(false, false, { value: '4'.ether, from: node }); - await shouldRevert(nodeDeposit(false, false, { - value: '2'.ether, - from: node, - }), 'Created validator', 'Bond requirement not met'); - await nodeDeposit(false, false, { value: '4'.ether, from: node }); - await shouldRevert(nodeDeposit(false, false, { - value: '2'.ether, - from: node, - }), 'Created validator', 'Bond requirement not met'); - await nodeDeposit(false, false, { value: '4'.ether, from: node }); - await shouldRevert(nodeDeposit(false, false, { - value: '2'.ether, - from: node, - }), 'Created validator', 'Bond requirement not met'); + await shouldRevert(nodeDeposit(node, '8'.ether), 'Created validator', 'Bond requirement not met'); + await shouldRevert(nodeDeposit(node, '2'.ether), 'Created validator', 'Bond requirement not met'); + await nodeDeposit(node); + await shouldRevert(nodeDeposit(node, '2'.ether), 'Created validator', 'Bond requirement not met'); + await nodeDeposit(node); + await shouldRevert(nodeDeposit(node, '2'.ether), 'Created validator', 'Bond requirement not met'); + await nodeDeposit(node); + await shouldRevert(nodeDeposit(node,'2'.ether), 'Created validator', 'Bond requirement not met'); }); it(printTitle('node', 'can not consume more than 2 provisioned express tickets'), async () => { - await nodeDeposit(true, false, { value: '4'.ether, from: node }); - await nodeDeposit(true, false, { value: '4'.ether, from: node }); - await shouldRevert(nodeDeposit(true, false, { - value: '4'.ether, - from: node, - }), 'Consumed express ticket', 'No express tickets'); + await nodeDeposit(node, '4'.ether, true); + await nodeDeposit(node, '4'.ether, true); + await shouldRevert(nodeDeposit(node, '4'.ether, true), 'Consumed express ticket', 'No express tickets'); }); it(printTitle('node', 'can create a new validator without an express ticket'), async () => { - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); }); it(printTitle('node', 'can create a new validator with an express ticket'), async () => { - await nodeDeposit(true, false, { value: '4'.ether, from: node }); + await nodeDeposit(node, '4'.ether, true); }); it(printTitle('random', 'can not dissolve validator before dissolve period ends'), async () => { - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); await shouldRevert(megapool.connect(random).dissolveValidator(0), 'Dissolved validator', 'Not enough time has passed to dissolve'); }); it(printTitle('random', 'can dissolve validator after dissolve period ends'), async () => { - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); await helpers.time.increase(dissolvePeriod + 1); await megapool.connect(random).dissolveValidator(0); }); it(printTitle('node', 'can perform stake operation on pre-stake validator'), async () => { - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); await stakeMegapoolValidator(megapool, 0, { from: node }); }); @@ -349,7 +343,7 @@ export default function() { before(async () => { await deployMegapool({ from: node }); - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); }); }); @@ -358,7 +352,7 @@ export default function() { before(async () => { await deployMegapool({ from: node }); - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); await stakeMegapoolValidator(megapool, 0, { from: node }); }); diff --git a/test/megapool/scenario-withdraw-credit.js b/test/megapool/scenario-withdraw-credit.js new file mode 100644 index 00000000..ba268581 --- /dev/null +++ b/test/megapool/scenario-withdraw-credit.js @@ -0,0 +1,39 @@ +import { RocketDepositPool, RocketStorage, RocketTokenRETH } from '../_utils/artifacts'; +import { assertBN } from '../_helpers/bn'; + +export async function withdrawCredit(node, amount) { + const rocketDepositPool = await RocketDepositPool.deployed(); + const rocketStorage = await RocketStorage.deployed(); + const rocketTokenRETH = await RocketTokenRETH.deployed(); + + async function getData() { + const [ + rethBalance, + creditBalance, + userBalance, + ] = await Promise.all([ + rocketTokenRETH.balanceOf(withdrawalAddress), + rocketDepositPool.getNodeCreditBalance(node.address), + rocketDepositPool.getUserBalance() + ]); + + return {rethBalance, creditBalance, userBalance}; + } + + const withdrawalAddress = await rocketStorage.getNodeWithdrawalAddress(node.address); + const rethValue = await rocketTokenRETH.getRethValue(amount); + + const dataBefore = await getData(); + + await rocketDepositPool.connect(node).withdrawCredit(amount); + + const dataAfter = await getData(); + + const rethBalanceDelta = dataAfter.rethBalance - dataBefore.rethBalance; + const creditBalanceDelta = dataAfter.creditBalance - dataBefore.creditBalance; + const userBalanceDelta = dataAfter.userBalance - dataBefore.userBalance; + + assertBN.equal(rethBalanceDelta, rethValue); + assertBN.equal(creditBalanceDelta, -amount); + assertBN.equal(userBalanceDelta, amount); +} \ No newline at end of file diff --git a/test/token/reth-tests.js b/test/token/reth-tests.js index 7c69611e..026ebcab 100644 --- a/test/token/reth-tests.js +++ b/test/token/reth-tests.js @@ -137,7 +137,7 @@ export default function() { await setNodeTrusted(trustedNode, 'saas_1', 'node@home.com', owner); // Register node and create a validator await registerNode({ from: node }); - await nodeDeposit(false, false, { value: '4'.ether, from: node }); + await nodeDeposit(node); await getMegapoolForNode(node); });