Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
a071001
feat: Allow Node Operators to Self Register Validators
vvalecha519 Oct 22, 2025
6fb1665
refactor: Simplify depositData handling in StakingManager for removin…
pankajjagtapp Oct 29, 2025
e141322
refactor: Update comment for clarity in IStakingManager interface reg…
pankajjagtapp Oct 29, 2025
4ca8f45
feat: Enhance PreludeTest with LiquidityPool integration and new vali…
pankajjagtapp Oct 29, 2025
900eaf7
feat: Add comprehensive tests for ValidatorKeyGen functionality in Li…
pankajjagtapp Oct 29, 2025
1a66baa
revert: restore test/prelude.t.sol to master version
pankajjagtapp Oct 30, 2025
b5b4461
feat: Implement tests for invalidating registered beacon validators i…
pankajjagtapp Oct 30, 2025
2505ac1
refactor: Update LiquidityPool contract to replace deprecated state v…
pankajjagtapp Oct 31, 2025
b095d3d
feat: Introduce OldLiquidityPool contract for testing upgrade compati…
pankajjagtapp Oct 31, 2025
0f214a0
refactor: Enhance ValidatorKeyGenTest with pre-upgrade storage value…
pankajjagtapp Oct 31, 2025
d644171
refactor: Introduce Custom Errors in LiquidityPool
pankajjagtapp Oct 31, 2025
c49a1d3
refactor: Remove unused error and update revert messages in Validator…
pankajjagtapp Oct 31, 2025
7de9b70
refactor: Update error handling in LiquidityPool and corresponding tests
pankajjagtapp Oct 31, 2025
2fcd0ed
revert: revert the changes made for the Liquidity Pool refactor
pankajjagtapp Nov 3, 2025
098cf09
fix: Add checks for already registered/invalidated validators in Stak…
pankajjagtapp Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/LiquidityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL

bytes32 public constant LIQUIDITY_POOL_ADMIN_ROLE = keccak256("LIQUIDITY_POOL_ADMIN_ROLE");
bytes32 public constant LIQUIDITY_POOL_VALIDATOR_APPROVER_ROLE = keccak256("LIQUIDITY_POOL_VALIDATOR_APPROVER_ROLE");
bytes32 public constant LIQUIDITY_POOL_VALIDATOR_CREATOR_ROLE = keccak256("LIQUIDITY_POOL_VALIDATOR_CREATOR_ROLE");

//--------------------------------------------------------------------------------------
//------------------------------------- EVENTS ---------------------------------------
Expand Down Expand Up @@ -279,8 +280,9 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL

// [Liquidty Pool Staking flow]
// Step 1: (Off-chain) create the keys using the desktop app
// Step 2: create validators with 1 eth deposits to official deposit contract
// Step 3: oracle approves and funds the remaining balance for the validator
// Step 2: register validator deposit data for later confirmation from the oracle before the 1eth deposit
// Step 3: create validators with 1 eth deposits to official deposit contract
// Step 4: oracle approves and funds the remaining balance for the validator

/// @notice claim bids and send 1 eth deposits to deposit contract to create the provided validators.
/// @dev step 2 of staking flow
Expand All @@ -290,6 +292,15 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL
address _etherFiNode
) external whenNotPaused {
require(validatorSpawner[msg.sender].registered, "Incorrect Caller");
stakingManager.registerBeaconValidators(_depositData, _bidIds, _etherFiNode);
}

function batchCreateBeaconValidators(
IStakingManager.DepositData[] calldata _depositData,
uint256[] calldata _bidIds,
address _etherFiNode
) external whenNotPaused {
if (!roleRegistry.hasRole(LIQUIDITY_POOL_VALIDATOR_CREATOR_ROLE, msg.sender)) revert IncorrectRole();

// liquidity pool supplies 1 eth per validator
uint256 outboundEthAmountFromLp = 1 ether * _bidIds.length;
Expand Down
64 changes: 48 additions & 16 deletions src/StakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,15 @@ contract StakingManager is

LegacyStakingManagerState legacyState; // all legacy state in this contract has been deprecated
mapping(address => bool) public deployedEtherFiNodes;
mapping(bytes32 => ValidatorCreationStatus) public validatorCreationStatus;

//---------------------------------------------------------------------------
//--------------------------- ROLES ---------------------------------------
//---------------------------------------------------------------------------

bytes32 public constant STAKING_MANAGER_NODE_CREATOR_ROLE = keccak256("STAKING_MANAGER_NODE_CREATOR_ROLE");
bytes32 public constant STAKING_MANAGER_ADMIN_ROLE = keccak256("STAKING_MANAGER_ADMIN_ROLE");
bytes32 public constant STAKING_MANAGER_VALIDATOR_INVALIDATOR_ROLE = keccak256("STAKING_MANAGER_VALIDATOR_INVALIDATOR_ROLE");

//-------------------------------------------------------------------------
//----------------------------- Admin -----------------------------------
Expand Down Expand Up @@ -88,39 +90,69 @@ contract StakingManager is
//---------------------------------------------------------------------------
//------------------------- Deposit Flow ------------------------------------
//---------------------------------------------------------------------------

function invalidateRegisteredBeaconValidator(DepositData calldata depositData, uint256 bidId, address etherFiNode) external {
if (!roleRegistry.hasRole(STAKING_MANAGER_VALIDATOR_INVALIDATOR_ROLE, msg.sender)) revert IncorrectRole();
bytes32 validatorCreationDataHash = keccak256(abi.encodePacked(depositData.publicKey, depositData.signature, depositData.depositDataRoot, depositData.ipfsHashForEncryptedValidatorKey, bidId, etherFiNode));
if (validatorCreationStatus[validatorCreationDataHash] != ValidatorCreationStatus.REGISTERED) revert InvalidValidatorCreationStatus();
validatorCreationStatus[validatorCreationDataHash] = ValidatorCreationStatus.INVALIDATED;
emit ValidatorCreationStatusUpdated(depositData, bidId, etherFiNode, validatorCreationDataHash, ValidatorCreationStatus.INVALIDATED);
}

/// @notice send 1 eth to deposit contract to create the validator.
/// @notice send 1 eth to deposit contract to create the validator.
/// The rest of the eth will not be sent until the oracle confirms the withdrawal credentials
/// @dev provided deposit data must be for a 0x02 compounding validator
function createBeaconValidators(DepositData[] calldata depositData, uint256[] calldata bidIds, address etherFiNode) external payable {
if (msg.sender != liquidityPool) revert InvalidCaller();
if (depositData.length != bidIds.length) revert InvalidDepositData();
if (address(IEtherFiNode(etherFiNode).getEigenPod()) == address(0)) revert InvalidEtherFiNode();

// process each 1 eth deposit to create validators for later verification from oracle
for (uint256 i = 0; i < depositData.length; i++) {
DepositData memory d = depositData[i];
bytes32 validatorCreationDataHash = keccak256(abi.encodePacked(d.publicKey, d.signature, d.depositDataRoot, d.ipfsHashForEncryptedValidatorKey, bidIds[i], etherFiNode));
if (validatorCreationStatus[validatorCreationDataHash] != ValidatorCreationStatus.REGISTERED) revert InvalidValidatorCreationStatus();

// claim the bid
if (!auctionManager.isBidActive(bidIds[i])) revert InactiveBid();
auctionManager.updateSelectedBidInformation(bidIds[i]);

// verify deposit root
bytes memory withdrawalCredentials = etherFiNodesManager.addressToCompoundingWithdrawalCredentials(address(IEtherFiNode(etherFiNode).getEigenPod()));
bytes32 computedDataRoot = generateDepositDataRoot(depositData[i].publicKey, depositData[i].signature, withdrawalCredentials, initialDepositAmount);
if (computedDataRoot != depositData[i].depositDataRoot) revert IncorrectBeaconRoot();
bytes32 computedDataRoot = generateDepositDataRoot(d.publicKey, d.signature, withdrawalCredentials, initialDepositAmount);
validatorCreationStatus[validatorCreationDataHash] = ValidatorCreationStatus.CONFIRMED;
auctionManager.updateSelectedBidInformation(bidIds[i]);

// Link the pubkey to a node. Will revert if this pubkey is already registered to a different target
etherFiNodesManager.linkPubkeyToNode(depositData[i].publicKey, etherFiNode, bidIds[i]);
etherFiNodesManager.linkPubkeyToNode(d.publicKey, etherFiNode, bidIds[i]);

// Deposit to the Beacon Chain
depositContractEth2.deposit{value: initialDepositAmount}(depositData[i].publicKey, withdrawalCredentials, depositData[i].signature, computedDataRoot);
depositContractEth2.deposit{value: initialDepositAmount}(d.publicKey, withdrawalCredentials, d.signature, computedDataRoot);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Beacon Deposit Data Validation Flaw

In createBeaconValidators, the computedDataRoot is recalculated but not validated against the depositDataRoot in the provided DepositData. If withdrawal credentials change between registration and creation, the beacon deposit might use different data than originally validated, potentially redirecting validator rewards.

Fix in Cursor Fix in Web


bytes32 pubkeyHash = calculateValidatorPubkeyHash(depositData[i].publicKey);
emit validatorCreated(pubkeyHash, etherFiNode, depositData[i].publicKey);
bytes32 pubkeyHash = calculateValidatorPubkeyHash(d.publicKey);
emit validatorCreated(pubkeyHash, etherFiNode, d.publicKey);
emit linkLegacyValidatorId(pubkeyHash, bidIds[i]); // can remove this once we fully transition to pubkeys

// legacy event for compatibility with existing tooling
emit ValidatorRegistered(auctionManager.getBidOwner(bidIds[i]), address(liquidityPool), address(liquidityPool), bidIds[i], depositData[i].publicKey, depositData[i].ipfsHashForEncryptedValidatorKey);
emit ValidatorRegistered(auctionManager.getBidOwner(bidIds[i]), address(liquidityPool), address(liquidityPool), bidIds[i], d.publicKey, d.ipfsHashForEncryptedValidatorKey);
emit ValidatorCreationStatusUpdated(d, bidIds[i], etherFiNode, validatorCreationDataHash, ValidatorCreationStatus.CONFIRMED);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Premature Validation and Incomplete Re-Validation Risks

The createBeaconValidators function omits re-validating the etherFiNode's EigenPod and depositDataRoot against registered values, risking invalid withdrawal credentials if they change. It also sets the validator's status to CONFIRMED prematurely, before critical operations like bid updates and the actual deposit, which could result in an inconsistent state or failed deposits.

Fix in Cursor Fix in Web

}

/// @notice register the beacon validators data for later confirmation from the oracle before the 1eth deposit
/// @dev provided deposit data must be for a 0x02 compounding validator
function registerBeaconValidators(DepositData[] calldata depositData, uint256[] calldata bidIds, address etherFiNode) external {
if (msg.sender != liquidityPool) revert InvalidCaller();
if (depositData.length != bidIds.length) revert InvalidDepositData();
if (address(IEtherFiNode(etherFiNode).getEigenPod()) == address(0) || !deployedEtherFiNodes[etherFiNode]) revert InvalidEtherFiNode();

// process each 1 eth deposit to create validators for later verification from oracle
for (uint256 i = 0; i < depositData.length; i++) {

// claim the bid
if (!auctionManager.isBidActive(bidIds[i])) revert InactiveBid();

// verify deposit root
bytes memory withdrawalCredentials = etherFiNodesManager.addressToCompoundingWithdrawalCredentials(address(IEtherFiNode(etherFiNode).getEigenPod()));
bytes32 computedDataRoot = generateDepositDataRoot(depositData[i].publicKey, depositData[i].signature, withdrawalCredentials, initialDepositAmount);
if (computedDataRoot != depositData[i].depositDataRoot) revert IncorrectBeaconRoot();

bytes32 validatorCreationDataHash = keccak256(abi.encodePacked(depositData[i].publicKey, depositData[i].signature, depositData[i].depositDataRoot, depositData[i].ipfsHashForEncryptedValidatorKey, bidIds[i], etherFiNode));
if (validatorCreationStatus[validatorCreationDataHash] != ValidatorCreationStatus.NOT_REGISTERED) revert InvalidValidatorCreationStatus();
validatorCreationStatus[validatorCreationDataHash] = ValidatorCreationStatus.REGISTERED;
emit ValidatorCreationStatusUpdated(depositData[i], bidIds[i], etherFiNode, validatorCreationDataHash, ValidatorCreationStatus.REGISTERED);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/interfaces/ILiquidityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ interface ILiquidityPool {
function requestMembershipNFTWithdraw(address recipient, uint256 amount, uint256 fee) external returns (uint256);

function batchRegister(IStakingManager.DepositData[] calldata _depositData, uint256[] calldata _bidIds, address _etherFiNode) external;
function batchCreateBeaconValidators(IStakingManager.DepositData[] calldata _depositData, uint256[] calldata _bidIds, address _etherFiNode) external;
function batchApproveRegistration(uint256[] memory _validatorIds, bytes[] calldata _pubkeys, bytes[] calldata _signatures) external;
function confirmAndFundBeaconValidators(IStakingManager.DepositData[] calldata depositData, uint256 validatorSizeWei) external;
function DEPRECATED_sendExitRequests(uint256[] calldata _validatorIds) external;
Expand Down
13 changes: 13 additions & 0 deletions src/interfaces/IStakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,18 @@ interface IStakingManager {
string ipfsHashForEncryptedValidatorKey;
}

// Possible values for validator creation status
enum ValidatorCreationStatus {
NOT_REGISTERED,
REGISTERED,
CONFIRMED,
INVALIDATED
}

// deposit flow
function registerBeaconValidators(DepositData[] calldata depositData, uint256[] calldata bidIds, address etherFiNode) external;
function createBeaconValidators(DepositData[] calldata depositData, uint256[] calldata bidIds, address etherFiNode) external payable;
function invalidateRegisteredBeaconValidator(DepositData calldata depositData, uint256 bidId, address etherFiNode) external;
function confirmAndFundBeaconValidators(DepositData[] calldata depositData, uint256 validatorSizeWei) external payable;
function calculateValidatorPubkeyHash(bytes memory pubkey) external pure returns (bytes32);
function initialDepositAmount() external returns (uint256);
Expand Down Expand Up @@ -78,6 +88,8 @@ interface IStakingManager {
// legacy event still being emitted in its original form to play nice with existing external tooling
event ValidatorRegistered(address indexed operator, address indexed bNftOwner, address indexed tNftOwner, uint256 validatorId, bytes validatorPubKey, string ipfsHashForEncryptedValidatorKey);

event ValidatorCreationStatusUpdated(DepositData depositData, uint256 bidId, address etherFiNode, bytes32 hashedAllData, ValidatorCreationStatus indexed status);

//--------------------------------------------------------------------------
//----------------------------- Errors -----------------------------------
//--------------------------------------------------------------------------
Expand All @@ -92,5 +104,6 @@ interface IStakingManager {
error InvalidValidatorSize();
error IncorrectRole();
error InvalidUpgrade();
error InvalidValidatorCreationStatus();

}
Loading
Loading