diff --git a/src/CSModule.sol b/src/CSModule.sol index fef24a2e..615d7bf0 100644 --- a/src/CSModule.sol +++ b/src/CSModule.sol @@ -83,8 +83,9 @@ contract CSModuleBase { uint256 targetValidatorsCount ); event WithdrawalSubmitted( - uint256 indexed validatorId, - uint256 withdrawalBalance + uint256 indexed nodeOperatorId, + uint256 keyIndex, + uint256 amount ); event BatchEnqueued( @@ -128,6 +129,8 @@ contract CSModuleBase { error QueueBatchUnvettedKeys(bytes32 batch); error SigningKeysInvalidOffset(); + + error WithdrawalAlreadySubmitted(); } contract CSModule is ICSModule, CSModuleBase { @@ -152,6 +155,7 @@ contract CSModule is ICSModule, CSModuleBase { bytes32 private _moduleType; uint256 private _nonce; mapping(uint256 => NodeOperator) private _nodeOperators; + mapping(uint256 noIdWithKeyIndex => bool) private _isValidatorWithdrawn; uint256 private _totalDepositedValidators; uint256 private _totalExitedValidators; @@ -1107,27 +1111,35 @@ contract CSModule is ICSModule, CSModuleBase { _checkForOutOfBond(nodeOperatorId); } + /// @notice Report node operator's key as withdrawn and settle withdrawn amount. + /// @param nodeOperatorId Operator ID in the module. + /// @param keyIndex Index of the withdrawn key in the node operator's keys. + /// @param amount Amount of withdrawn ETH in wei. function submitWithdrawal( - bytes32 /*withdrawalProof*/, uint256 nodeOperatorId, - uint256 validatorId, - uint256 withdrawalBalance - ) external onlyExistingNodeOperator(nodeOperatorId) { - // TODO: check for withdrawal proof - // TODO: consider asserting that withdrawn keys count is not higher than exited keys count + uint256 keyIndex, + uint256 amount + ) external onlyExistingNodeOperator(nodeOperatorId) onlyWithdrawalReporter { NodeOperator storage no = _nodeOperators[nodeOperatorId]; + if (keyIndex >= no.totalDepositedKeys) { + revert SigningKeysInvalidOffset(); + } - no.totalWithdrawnKeys += 1; + // NOTE: both nodeOperatorId and keyIndex are limited to uint64 by the contract. + uint256 pointer = (nodeOperatorId << 128) | keyIndex; + if (_isValidatorWithdrawn[pointer]) { + revert WithdrawalAlreadySubmitted(); + } - if (withdrawalBalance < DEPOSIT_SIZE) { - accounting.penalize( - nodeOperatorId, - DEPOSIT_SIZE - withdrawalBalance - ); + _isValidatorWithdrawn[pointer] = true; + no.totalWithdrawnKeys++; + + if (amount < DEPOSIT_SIZE) { + accounting.penalize(nodeOperatorId, DEPOSIT_SIZE - amount); _checkForOutOfBond(nodeOperatorId); } - emit WithdrawalSubmitted(validatorId, withdrawalBalance); + emit WithdrawalSubmitted(nodeOperatorId, keyIndex, amount); } /// @notice Called when withdrawal credentials changed by DAO @@ -1450,4 +1462,10 @@ contract CSModule is ICSModule, CSModuleBase { // TODO: check the role _; } + + modifier onlyWithdrawalReporter() { + // Here should be a role granted to the CSVerifier contract and/or to the DAO/Oracle. + // TODO: check the role + _; + } } diff --git a/src/interfaces/ICSModule.sol b/src/interfaces/ICSModule.sol index 7bdedb7a..7fcaf2a3 100644 --- a/src/interfaces/ICSModule.sol +++ b/src/interfaces/ICSModule.sol @@ -23,4 +23,25 @@ interface ICSModule is IStakingModule { function getNodeOperator( uint256 nodeOperatorId ) external view returns (NodeOperatorInfo memory); + + /// @notice Gets node operator signing keys + /// @param nodeOperatorId ID of the node operator + /// @param startIndex Index of the first key + /// @param keysCount Count of keys to get + /// @return Signing keys + function getNodeOperatorSigningKeys( + uint256 nodeOperatorId, + uint256 startIndex, + uint256 keysCount + ) external view returns (bytes memory); + + /// @notice Report node operator's key as withdrawn and settle withdrawn amount. + /// @param nodeOperatorId Operator ID in the module. + /// @param keyIndex Index of the withdrawn key in the node operator's keys. + /// @param amount Amount of withdrawn ETH in wei. + function submitWithdrawal( + uint256 nodeOperatorId, + uint256 keyIndex, + uint256 amount + ) external; } diff --git a/test/CSModule.t.sol b/test/CSModule.t.sol index e91625c5..e99d92f7 100644 --- a/test/CSModule.t.sol +++ b/test/CSModule.t.sol @@ -1889,21 +1889,21 @@ contract CsmSettleELRewardsStealingPenalty is CSMCommon { contract CsmSubmitWithdrawal is CSMCommon { function test_submitWithdrawal() public { - uint256 validatorId = 1; + uint256 keyIndex = 0; uint256 noId = createNodeOperator(); csm.vetKeys(noId, 1); csm.obtainDepositData(1, ""); vm.expectEmit(true, true, true, true, address(csm)); - emit WithdrawalSubmitted(validatorId, csm.DEPOSIT_SIZE()); - csm.submitWithdrawal("", noId, validatorId, csm.DEPOSIT_SIZE()); + emit WithdrawalSubmitted(noId, keyIndex, csm.DEPOSIT_SIZE()); + csm.submitWithdrawal(noId, keyIndex, csm.DEPOSIT_SIZE()); CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalWithdrawnValidators, 1); } function test_submitWithdrawal_lowExitBalance() public { - uint256 validatorId = 1; + uint256 keyIndex = 0; uint256 noId = createNodeOperator(); uint256 depositSize = csm.DEPOSIT_SIZE(); csm.vetKeys(noId, 1); @@ -1913,11 +1913,11 @@ contract CsmSubmitWithdrawal is CSMCommon { address(accounting), abi.encodeWithSelector(accounting.penalize.selector, noId, 1 ether) ); - csm.submitWithdrawal("", noId, validatorId, depositSize - 1 ether); + csm.submitWithdrawal(noId, keyIndex, depositSize - 1 ether); } function test_submitWithdrawal_RevertWhenNoNodeOperator() public { vm.expectRevert(NodeOperatorDoesNotExist.selector); - csm.submitWithdrawal("", 0, 0, 0); + csm.submitWithdrawal(0, 0, 0); } }