Skip to content

Commit 1acc195

Browse files
authored
feat: Add batch withdrawals reporting (#429)
1 parent 9c7ec46 commit 1acc195

8 files changed

+247
-79
lines changed

script/fork-helpers/NodeOperators.s.sol

+10-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ForkHelpersCommon } from "./Common.sol";
99
import "../../src/interfaces/IVEBO.sol";
1010
import { Utilities } from "../../test/helpers/Utilities.sol";
1111
import { IStakingRouter } from "../../src/interfaces/IStakingRouter.sol";
12-
import { NodeOperator } from "../../src/interfaces/ICSModule.sol";
12+
import { NodeOperator, ValidatorWithdrawalInfo } from "../../src/interfaces/ICSModule.sol";
1313

1414
contract NodeOperators is
1515
Script,
@@ -200,7 +200,15 @@ contract NodeOperators is
200200
) external broadcastVerifier {
201201
uint256 withdrawnBefore = csm.getNodeOperator(noId).totalWithdrawnKeys;
202202

203-
csm.submitWithdrawal(noId, keyIndex, amount, false);
203+
ValidatorWithdrawalInfo[]
204+
memory withdrawalInfo = new ValidatorWithdrawalInfo[](1);
205+
withdrawalInfo[0] = ValidatorWithdrawalInfo(
206+
noId,
207+
keyIndex,
208+
amount,
209+
false
210+
);
211+
csm.submitWithdrawals(withdrawalInfo);
204212

205213
assertTrue(csm.isValidatorWithdrawn(noId, keyIndex));
206214
assertEq(

src/CSModule.sol

+52-32
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { ILidoLocator } from "./interfaces/ILidoLocator.sol";
1313
import { IStETH } from "./interfaces/IStETH.sol";
1414
import { ICSParametersRegistry } from "./interfaces/ICSParametersRegistry.sol";
1515
import { ICSAccounting } from "./interfaces/ICSAccounting.sol";
16-
import { ICSModule, NodeOperator, NodeOperatorManagementProperties } from "./interfaces/ICSModule.sol";
16+
import { ICSModule, NodeOperator, NodeOperatorManagementProperties, ValidatorWithdrawalInfo } from "./interfaces/ICSModule.sol";
1717

1818
import { QueueLib, Batch } from "./lib/QueueLib.sol";
1919
import { ValidatorCountsReport } from "./lib/ValidatorCountsReport.sol";
@@ -677,45 +677,65 @@ contract CSModule is
677677
}
678678

679679
/// @inheritdoc ICSModule
680-
function submitWithdrawal(
681-
uint256 nodeOperatorId,
682-
uint256 keyIndex,
683-
uint256 amount,
684-
bool isSlashed
680+
function submitWithdrawals(
681+
ValidatorWithdrawalInfo[] calldata withdrawalsInfo
685682
) external onlyRole(VERIFIER_ROLE) {
686-
_onlyExistingNodeOperator(nodeOperatorId);
687-
NodeOperator storage no = _nodeOperators[nodeOperatorId];
688-
if (keyIndex >= no.totalDepositedKeys) {
689-
revert SigningKeysInvalidOffset();
690-
}
691-
692-
uint256 pointer = _keyPointer(nodeOperatorId, keyIndex);
693-
if (_isValidatorWithdrawn[pointer]) revert AlreadyWithdrawn();
683+
for (uint256 i; i < withdrawalsInfo.length; ++i) {
684+
ValidatorWithdrawalInfo memory withdrawalInfo = withdrawalsInfo[i];
694685

695-
_isValidatorWithdrawn[pointer] = true;
696-
unchecked {
697-
++no.totalWithdrawnKeys;
698-
}
686+
_onlyExistingNodeOperator(withdrawalInfo.nodeOperatorId);
687+
NodeOperator storage no = _nodeOperators[
688+
withdrawalInfo.nodeOperatorId
689+
];
699690

700-
bytes memory pubkey = SigningKeys.loadKeys(nodeOperatorId, keyIndex, 1);
701-
emit WithdrawalSubmitted(nodeOperatorId, keyIndex, amount, pubkey);
691+
if (withdrawalInfo.keyIndex >= no.totalDepositedKeys) {
692+
revert SigningKeysInvalidOffset();
693+
}
702694

703-
if (isSlashed) {
704-
// Bond curve should be reset to default in case of slashing. See https://hackmd.io/@lido/SygBLW5ja
705-
accounting.resetBondCurve(nodeOperatorId);
706-
}
695+
uint256 pointer = _keyPointer(
696+
withdrawalInfo.nodeOperatorId,
697+
withdrawalInfo.keyIndex
698+
);
699+
if (_isValidatorWithdrawn[pointer]) revert AlreadyWithdrawn();
707700

708-
if (DEPOSIT_SIZE > amount) {
701+
_isValidatorWithdrawn[pointer] = true;
709702
unchecked {
710-
accounting.penalize(nodeOperatorId, DEPOSIT_SIZE - amount);
703+
++no.totalWithdrawnKeys;
711704
}
712-
}
713705

714-
// Nonce should be updated if depositableValidators change
715-
_updateDepositableValidatorsCount({
716-
nodeOperatorId: nodeOperatorId,
717-
incrementNonceIfUpdated: true
718-
});
706+
bytes memory pubkey = SigningKeys.loadKeys(
707+
withdrawalInfo.nodeOperatorId,
708+
withdrawalInfo.keyIndex,
709+
1
710+
);
711+
712+
emit WithdrawalSubmitted(
713+
withdrawalInfo.nodeOperatorId,
714+
withdrawalInfo.keyIndex,
715+
withdrawalInfo.amount,
716+
pubkey
717+
);
718+
719+
if (withdrawalInfo.isSlashed) {
720+
// Bond curve should be reset to default in case of slashing. See https://hackmd.io/@lido/SygBLW5ja
721+
accounting.resetBondCurve(withdrawalInfo.nodeOperatorId);
722+
}
723+
724+
if (DEPOSIT_SIZE > withdrawalInfo.amount) {
725+
unchecked {
726+
accounting.penalize(
727+
withdrawalInfo.nodeOperatorId,
728+
DEPOSIT_SIZE - withdrawalInfo.amount
729+
);
730+
}
731+
}
732+
733+
// Nonce should be updated if depositableValidators change
734+
_updateDepositableValidatorsCount({
735+
nodeOperatorId: withdrawalInfo.nodeOperatorId,
736+
incrementNonceIfUpdated: true
737+
});
738+
}
719739
}
720740

721741
/// @inheritdoc IStakingModule

src/CSVerifier.sol

+9-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
pragma solidity 0.8.24;
55

66
import { ICSVerifier } from "./interfaces/ICSVerifier.sol";
7-
import { ICSModule } from "./interfaces/ICSModule.sol";
7+
import { ICSModule, ValidatorWithdrawalInfo } from "./interfaces/ICSModule.sol";
88
import { AccessControlEnumerable } from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol";
99
import { PausableUntil } from "./lib/utils/PausableUntil.sol";
1010

@@ -150,12 +150,15 @@ contract CSVerifier is ICSVerifier, AccessControlEnumerable, PausableUntil {
150150
pubkey: pubkey
151151
});
152152

153-
MODULE.submitWithdrawal(
153+
ValidatorWithdrawalInfo[]
154+
memory withdrawalsInfo = new ValidatorWithdrawalInfo[](1);
155+
withdrawalsInfo[0] = ValidatorWithdrawalInfo(
154156
nodeOperatorId,
155157
keyIndex,
156158
withdrawalAmount,
157159
witness.slashed
158160
);
161+
MODULE.submitWithdrawals(withdrawalsInfo);
159162
}
160163

161164
/// @inheritdoc ICSVerifier
@@ -214,12 +217,15 @@ contract CSVerifier is ICSVerifier, AccessControlEnumerable, PausableUntil {
214217
pubkey: pubkey
215218
});
216219

217-
MODULE.submitWithdrawal(
220+
ValidatorWithdrawalInfo[]
221+
memory withdrawalsInfo = new ValidatorWithdrawalInfo[](1);
222+
withdrawalsInfo[0] = ValidatorWithdrawalInfo(
218223
nodeOperatorId,
219224
keyIndex,
220225
withdrawalAmount,
221226
witness.slashed
222227
);
228+
MODULE.submitWithdrawals(withdrawalsInfo);
223229
}
224230

225231
function _getParentBlockRoot(

src/interfaces/ICSModule.sol

+11-10
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ struct NodeOperatorManagementProperties {
3939
bool extendedManagerPermissions;
4040
}
4141

42+
struct ValidatorWithdrawalInfo {
43+
uint256 nodeOperatorId; // @dev ID of the Node Operator
44+
uint256 keyIndex; // @dev Index of the withdrawn key in the Node Operator's keys storage
45+
uint256 amount; // @dev Amount of withdrawn ETH in wei
46+
bool isSlashed; // @dev If validator is slashed or not
47+
}
48+
4249
/// @title Lido's Community Staking Module interface
4350
interface ICSModule is IQueueLib, INOAddresses, IAssetRecovererLib {
4451
error NodeOperatorHasKeys();
@@ -412,18 +419,12 @@ interface ICSModule is IQueueLib, INOAddresses, IAssetRecovererLib {
412419
uint256 keysCount
413420
) external view returns (bytes memory keys, bytes memory signatures);
414421

415-
/// @notice Report Node Operator's key as withdrawn and settle withdrawn amount
422+
/// @notice Report Node Operator's keys as withdrawn and settle withdrawn amount
416423
/// @notice Called by `CSVerifier` contract.
417424
/// See `CSVerifier.processWithdrawalProof` to use this method permissionless
418-
/// @param nodeOperatorId ID of the Node Operator
419-
/// @param keyIndex Index of the withdrawn key in the Node Operator's keys storage
420-
/// @param amount Amount of withdrawn ETH in wei
421-
/// @param isSlashed Validator is slashed or not
422-
function submitWithdrawal(
423-
uint256 nodeOperatorId,
424-
uint256 keyIndex,
425-
uint256 amount,
426-
bool isSlashed
425+
/// @param withdrawalsInfo An array for the validator withdrawals info structs
426+
function submitWithdrawals(
427+
ValidatorWithdrawalInfo[] calldata withdrawalsInfo
427428
) external;
428429

429430
/// @notice Check if the given Node Operator's key is reported as withdrawn

0 commit comments

Comments
 (0)