Skip to content

Commit efea0dd

Browse files
authored
Merge pull request #1482 from lidofinance/fix/pdg-side-activation
[VAULTS] PDG side-activation
2 parents 9875250 + 82c8b79 commit efea0dd

File tree

7 files changed

+98
-69
lines changed

7 files changed

+98
-69
lines changed

contracts/0.8.25/vaults/LazyOracle.sol

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
106106

107107
struct VaultInfo {
108108
address vault;
109-
uint256 aggregateBalance; // includes pendingPredeposits, availableBalance and stagedBalance
109+
uint256 aggregatedBalance; // includes availableBalance and stagedBalance
110110
int256 inOutDelta;
111111
bytes32 withdrawalCredentials;
112112
uint256 liabilityShares;
@@ -231,7 +231,7 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
231231
VaultHub.VaultRecord memory record = vaultHub.vaultRecord(vaultAddress);
232232
batch[i] = VaultInfo(
233233
vaultAddress,
234-
vault.availableBalance() + vault.stagedBalance() + pendingPredeposits(vault),
234+
vault.availableBalance() + vault.stagedBalance(),
235235
record.inOutDelta.currentValue(),
236236
vault.withdrawalCredentials(),
237237
record.liabilityShares,
@@ -249,6 +249,20 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
249249
return batch;
250250
}
251251

252+
/**
253+
* @notice batch method to mass check the validator stages in PredepositGuarantee contract
254+
* @param _pubkeys the array of validator's pubkeys to check
255+
*/
256+
function batchValidatorStages(
257+
bytes[] calldata _pubkeys
258+
) external view returns (IPredepositGuarantee.ValidatorStage[] memory batch) {
259+
batch = new IPredepositGuarantee.ValidatorStage[](_pubkeys.length);
260+
261+
for (uint256 i = 0; i < _pubkeys.length; i++) {
262+
batch[i] = predepositGuarantee().validatorStatus(_pubkeys[i]).stage;
263+
}
264+
}
265+
252266
/// @notice update the sanity parameters
253267
/// @param _quarantinePeriod the quarantine period
254268
/// @param _maxRewardRatioBP the max EL CL rewards
@@ -562,8 +576,8 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
562576
}
563577
}
564578

565-
function pendingPredeposits(IStakingVault _vault) internal view returns (uint256) {
566-
return IPredepositGuarantee(LIDO_LOCATOR.predepositGuarantee()).pendingPredeposits(_vault);
579+
function predepositGuarantee() internal view returns (IPredepositGuarantee) {
580+
return IPredepositGuarantee(LIDO_LOCATOR.predepositGuarantee());
567581
}
568582

569583
function _vaultHub() internal view returns (VaultHub) {

contracts/0.8.25/vaults/VaultHub.sol

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,8 @@ contract VaultHub is PausableUntilWithRoles {
353353
if (vault_.pendingOwner() != address(this)) revert VaultHubNotPendingOwner(_vault);
354354
if (IPinnedBeaconProxy(address(vault_)).isOssified()) revert VaultOssified(_vault);
355355
if (vault_.depositor() != address(_predepositGuarantee())) revert PDGNotDepositor(_vault);
356-
// for each pending predeposit, vault should have an activation amount staged in StakingVault
357-
// 1 predeposit is 1 ether and activation amount is 31 ether
358-
if (vault_.stagedBalance() != 31 * _predepositGuarantee().pendingPredeposits(vault_)) {
356+
// we need vault to match staged balance with pendingActivations
357+
if (vault_.stagedBalance() != _predepositGuarantee().pendingActivations(vault_) * 31 ether) {
359358
revert InsufficientStagedBalance(_vault);
360359
}
361360

contracts/0.8.25/vaults/interfaces/IPredepositGuarantee.sol

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,33 @@ import {IStakingVault} from "./IStakingVault.sol";
1313
* @notice Interface for the `PredepositGuarantee` contract
1414
*/
1515
interface IPredepositGuarantee {
16+
/**
17+
* @notice represents validator stages in PDG flow
18+
* @param NONE - initial stage
19+
* @param PREDEPOSITED - PREDEPOSIT_AMOUNT is deposited to this validator by the vault
20+
* @param PROVEN - validator is proven to be valid and can be used to deposit to beacon chain
21+
* @param ACTIVATED - validator is proven and the ACTIVATION_DEPOSIT_AMOUNT is deposited to this validator
22+
* @param COMPENSATED - disproven validator has its PREDEPOSIT_AMOUNT ether compensated to staking vault owner and validator cannot be used in PDG anymore
23+
*/
24+
enum ValidatorStage {
25+
NONE,
26+
PREDEPOSITED,
27+
PROVEN,
28+
ACTIVATED,
29+
COMPENSATED
30+
}
31+
/**
32+
* @notice represents status of the validator in PDG
33+
* @param stage represents validator stage in PDG flow
34+
* @param stakingVault pins validator to specific StakingVault
35+
* @param nodeOperator pins validator to specific NO
36+
*/
37+
struct ValidatorStatus {
38+
ValidatorStage stage;
39+
IStakingVault stakingVault;
40+
address nodeOperator;
41+
}
42+
1643
/**
1744
* @notice user input for validator proof verification
1845
* @custom:proof array of merkle proofs from parent(pubkey,wc) node to Beacon block root
@@ -31,6 +58,7 @@ interface IPredepositGuarantee {
3158
uint64 proposerIndex;
3259
}
3360

34-
function pendingPredeposits(IStakingVault _vault) external view returns (uint256);
61+
function pendingActivations(IStakingVault _vault) external view returns (uint256);
62+
function validatorStatus(bytes calldata _pubkey) external view returns (ValidatorStatus memory);
3563
function proveUnknownValidator(ValidatorWitness calldata _witness, IStakingVault _stakingVault) external;
3664
}

contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,7 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU
6060
mapping(address guarantor => uint256 claimableEther) guarantorClaimableEther;
6161
mapping(bytes validatorPubkey => ValidatorStatus validatorStatus) validatorStatus;
6262
mapping(address nodeOperator => address depositor) nodeOperatorDepositor;
63-
mapping(address stakingVault => uint256 balance) pendingPredeposits;
64-
}
65-
66-
/**
67-
* @notice represents validator stages in PDG flow
68-
* @param NONE - initial stage
69-
* @param PREDEPOSITED - PREDEPOSIT_AMOUNT is deposited to this validator by the vault
70-
* @param PROVEN - validator is proven to be valid and can be used to deposit to beacon chain
71-
* @param ACTIVATED - validator is proven and the ACTIVATION_DEPOSIT_AMOUNT is deposited to this validator
72-
* @param COMPENSATED - disproven validator has its PREDEPOSIT_AMOUNT ether compensated to staking vault owner and validator cannot be used in PDG anymore
73-
*/
74-
enum ValidatorStage {
75-
NONE,
76-
PREDEPOSITED,
77-
PROVEN,
78-
ACTIVATED,
79-
COMPENSATED
63+
mapping(address stakingVault => uint256 number) pendingActivations;
8064
}
8165

8266
/**
@@ -90,18 +74,6 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU
9074
uint128 locked;
9175
}
9276

93-
/**
94-
* @notice represents status of the validator in PDG
95-
* @param stage represents validator stage in PDG flow
96-
* @param stakingVault pins validator to specific StakingVault
97-
* @param nodeOperator pins validator to specific NO
98-
*/
99-
struct ValidatorStatus {
100-
ValidatorStage stage;
101-
IStakingVault stakingVault;
102-
address nodeOperator;
103-
}
104-
10577
/**
10678
* @notice encodes parameters for method "topUpExistingValidators"
10779
* @param pubkey public key of the validator to top up. It should have the PROVEN status
@@ -220,17 +192,19 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU
220192
* @param _validatorPubkey to check status for
221193
* @return struct of ValidatorStatus
222194
*/
223-
function validatorStatus(bytes calldata _validatorPubkey) external view returns (ValidatorStatus memory) {
195+
function validatorStatus(
196+
bytes calldata _validatorPubkey
197+
) external view override returns (ValidatorStatus memory) {
224198
return _storage().validatorStatus[_validatorPubkey];
225199
}
226200

227201
/**
228-
* @notice returns the current amount of ether that is predeposited to a given vault
202+
* @notice returns the number of validators in PREDEPOSITED and PROVEN states but not ACTIVATED yet
229203
* @param _vault staking vault address
230-
* @return amount of ether in wei
204+
* @return the number of validators yet-to-be-activated
231205
*/
232-
function pendingPredeposits(IStakingVault _vault) external view returns (uint256) {
233-
return _storage().pendingPredeposits[address(_vault)];
206+
function pendingActivations(IStakingVault _vault) external view returns (uint256) {
207+
return _storage().pendingActivations[address(_vault)];
234208
}
235209

236210
/**
@@ -415,7 +389,7 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU
415389
balance.locked += uint128(totalDepositAmount);
416390
emit BalanceLocked(nodeOperator, balance.total, balance.locked);
417391

418-
$.pendingPredeposits[address(_stakingVault)] += _deposits.length * PREDEPOSIT_AMOUNT;
392+
$.pendingActivations[address(_stakingVault)] += _deposits.length;
419393

420394
mapping(bytes validatorPubkey => ValidatorStatus) storage validatorByPubkey = _storage().validatorStatus;
421395

@@ -446,12 +420,13 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU
446420
}
447421

448422
/**
449-
* @notice permissionless method to prove correct Withdrawal Credentials for the validator
423+
* @notice permissionless method to prove correct Withdrawal Credentials and activate validator if possible
450424
* @param _witness object containing validator pubkey, Merkle proof and timestamp for Beacon Block root child block
451425
* @dev will revert if proof is invalid or misformed or validator is not predeposited
452-
* @dev transition PREDEPOSITED => PROVEN
426+
* @dev transition PREDEPOSITED => PROVEN [=> ACTIVATED]
427+
* @dev if activation is impossible, it can be done later by calling activateValidator() explicitly
453428
*/
454-
function proveWC(ValidatorWitness calldata _witness) external whenResumed {
429+
function proveWCAndActivate(ValidatorWitness calldata _witness) external whenResumed {
455430
ValidatorStatus storage validator = _storage().validatorStatus[_witness.pubkey];
456431

457432
if (validator.stage != ValidatorStage.PREDEPOSITED) {
@@ -460,9 +435,19 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU
460435

461436
IStakingVault stakingVault = validator.stakingVault;
462437
bytes32 withdrawalCredentials = _checkVaultWC(stakingVault);
438+
address nodeOperator = validator.nodeOperator;
463439

464-
validator.stage = ValidatorStage.PROVEN;
465-
_proveWC(_witness, stakingVault, withdrawalCredentials, validator.nodeOperator);
440+
_proveWC(_witness, stakingVault, withdrawalCredentials, nodeOperator);
441+
442+
// activate validator if possible
443+
if (stakingVault.depositor() == address(this) && stakingVault.stagedBalance() >= ACTIVATION_DEPOSIT_AMOUNT) {
444+
_activateAndTopUpValidator(stakingVault, _witness.pubkey, 0, new bytes(96), withdrawalCredentials, nodeOperator);
445+
validator.stage = ValidatorStage.ACTIVATED;
446+
} else {
447+
// only if validator is disconnected
448+
// because on connection we check depositor and staged balance
449+
validator.stage = ValidatorStage.PROVEN;
450+
}
466451
}
467452

468453
/**
@@ -576,7 +561,7 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU
576561
NodeOperatorBalance storage balance = $.nodeOperatorBalance[nodeOperator];
577562
balance.total -= PREDEPOSIT_AMOUNT;
578563
balance.locked -= PREDEPOSIT_AMOUNT;
579-
$.pendingPredeposits[address(stakingVault)] -= PREDEPOSIT_AMOUNT;
564+
$.pendingActivations[address(stakingVault)] -= 1;
580565

581566
// unlocking the staged amount if possible as we are not activating this validator
582567
if (stakingVault.depositor() == address(this) && stakingVault.stagedBalance() >= ACTIVATION_DEPOSIT_AMOUNT) {
@@ -706,7 +691,6 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU
706691

707692
NodeOperatorBalance storage balance = _storage().nodeOperatorBalance[_nodeOperator];
708693
balance.locked -= PREDEPOSIT_AMOUNT;
709-
_storage().pendingPredeposits[address(_vault)] -= PREDEPOSIT_AMOUNT;
710694

711695
emit BalanceUnlocked(_nodeOperator, balance.total, balance.locked);
712696
emit ValidatorProven(_witness.pubkey, _nodeOperator, address(_vault), _withdrawalCredentials);
@@ -720,6 +704,7 @@ contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableU
720704
bytes32 _withdrawalCredentials,
721705
address _nodeOperator
722706
) internal {
707+
_storage().pendingActivations[address(_stakingVault)] -= 1;
723708
uint256 depositAmount = ACTIVATION_DEPOSIT_AMOUNT + _additionalAmount;
724709

725710
IStakingVault.Deposit memory deposit = IStakingVault.Deposit({

test/0.8.25/vaults/lazyOracle/contracts/PredepositGuarantee__MockForLazyOracle.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {IPredepositGuarantee} from "contracts/0.8.25/vaults/interfaces/IPredepos
77
import {IStakingVault} from "contracts/0.8.25/vaults/interfaces/IStakingVault.sol";
88

99
contract PredepositGuarantee__MockForLazyOracle is IPredepositGuarantee {
10-
function pendingPredeposits(IStakingVault _vault) external view override returns (uint256) {}
10+
function pendingActivations(IStakingVault _vault) external view override returns (uint256) {}
1111

1212
function proveUnknownValidator(ValidatorWitness calldata _witness, IStakingVault _stakingVault) external override {}
13+
14+
function validatorStatus(bytes calldata _pubkey) external view override returns (ValidatorStatus memory) {}
1315
}

test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ describe("LazyOracle.sol", () => {
171171

172172
const vaultInfo = vaults[0];
173173
expect(vaultInfo.vault).to.equal(vault1);
174-
expect(vaultInfo.aggregateBalance).to.equal(0n);
174+
expect(vaultInfo.aggregatedBalance).to.equal(0n);
175175
expect(vaultInfo.inOutDelta).to.equal(5n);
176176
expect(vaultInfo.withdrawalCredentials).to.equal(ZERO_BYTES32);
177177
expect(vaultInfo.liabilityShares).to.equal(4n);

test/0.8.25/vaults/predepositGuarantee/predepositGuarantee.test.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -851,27 +851,29 @@ describe("PredepositGuarantee.sol", () => {
851851
};
852852

853853
// stage NONE
854-
await expect(pdg.proveWC(witness))
854+
await expect(pdg.proveWCAndActivate(witness))
855855
.to.be.revertedWithCustomError(pdg, "ValidatorNotPreDeposited")
856856
.withArgs(validator.container.pubkey, 0n);
857857

858858
// stage PREDEPOSITED
859859
const { deposit, depositY } = await generatePredeposit(validator);
860860
await pdg.predeposit(stakingVault, [deposit], [depositY]);
861861

862-
const proveTx = await pdg.proveWC(witness);
862+
const proveTx = await pdg.proveWCAndActivate(witness);
863863
await expect(proveTx)
864864
.to.emit(pdg, "BalanceUnlocked")
865865
.withArgs(vaultOperator.address, balance, 0)
866866
.to.emit(pdg, "ValidatorProven")
867+
.withArgs(validator.container.pubkey, vaultOperator.address, await stakingVault.getAddress(), wc)
868+
.to.emit(pdg, "ValidatorActivated")
867869
.withArgs(validator.container.pubkey, vaultOperator.address, await stakingVault.getAddress(), wc);
868870

869-
expect((await pdg.validatorStatus(validator.container.pubkey)).stage).to.equal(2n);
871+
expect((await pdg.validatorStatus(validator.container.pubkey)).stage).to.equal(3n); // 3n is ACTIVATED
870872

871-
// stage PROVEN
872-
await expect(pdg.proveWC(witness))
873+
// stage ACTIVATED
874+
await expect(pdg.proveWCAndActivate(witness))
873875
.to.be.revertedWithCustomError(pdg, "ValidatorNotPreDeposited")
874-
.withArgs(validator.container.pubkey, 2n);
876+
.withArgs(validator.container.pubkey, 3n); // 3n is ACTIVATED
875877
});
876878

877879
it("allows NO to proveValidatorWC", async () => {
@@ -918,16 +920,18 @@ describe("PredepositGuarantee.sol", () => {
918920
proposerIndex: beaconBlockHeader.proposerIndex,
919921
};
920922

921-
const proveValidatorWCTX = pdg.connect(vaultOwner).proveWC(witness);
923+
const proveValidatorWCTX = pdg.connect(vaultOwner).proveWCAndActivate(witness);
922924

923925
await expect(proveValidatorWCTX)
924926
.to.emit(pdg, "BalanceUnlocked")
925927
.withArgs(vaultOperator, ether("1"), ether("0"))
926928
.to.emit(pdg, "ValidatorProven")
929+
.withArgs(validator.container.pubkey, vaultOperator, stakingVault, vaultWC)
930+
.to.emit(pdg, "ValidatorActivated")
927931
.withArgs(validator.container.pubkey, vaultOperator, stakingVault, vaultWC);
928932

929933
const validatorStatus = await pdg.validatorStatus(validator.container.pubkey);
930-
expect(validatorStatus.stage).to.equal(2n);
934+
expect(validatorStatus.stage).to.equal(3n); // 3n is ACTIVATED
931935
expect(validatorStatus.stakingVault).to.equal(stakingVault);
932936
expect(validatorStatus.nodeOperator).to.equal(vaultOperator);
933937
});
@@ -1017,35 +1021,32 @@ describe("PredepositGuarantee.sol", () => {
10171021
const sameNoProof = [...sameNoValidatorProof.proof, ...sameNoStateProof, ...beaconProof];
10181022

10191023
// prove
1020-
await pdg.proveWC({
1024+
await pdg.proveWCAndActivate({
10211025
proof: mainProof,
10221026
pubkey: mainValidator.container.pubkey,
10231027
validatorIndex: mainValidatorIndex,
10241028
childBlockTimestamp: childBlockTimestamp,
10251029
slot: beaconHeader.slot,
10261030
proposerIndex: beaconHeader.proposerIndex,
10271031
});
1028-
await pdg.activateValidator(mainValidator.container.pubkey);
10291032

1030-
await pdg.proveWC({
1033+
await pdg.proveWCAndActivate({
10311034
proof: sideProof,
10321035
pubkey: sideValidator.container.pubkey,
10331036
validatorIndex: sideValidatorIndex,
10341037
childBlockTimestamp: childBlockTimestamp,
10351038
slot: beaconHeader.slot,
10361039
proposerIndex: beaconHeader.proposerIndex,
10371040
});
1038-
await pdg.activateValidator(sideValidator.container.pubkey);
10391041

1040-
await pdg.proveWC({
1042+
await pdg.proveWCAndActivate({
10411043
proof: sameNoProof,
10421044
pubkey: sameNOValidator.container.pubkey,
10431045
validatorIndex: sameNoValidatorIndex,
10441046
childBlockTimestamp: childBlockTimestamp,
10451047
slot: beaconHeader.slot,
10461048
proposerIndex: beaconHeader.proposerIndex,
10471049
});
1048-
await pdg.activateValidator(sameNOValidator.container.pubkey);
10491050

10501051
expect((await pdg.validatorStatus(mainValidator.container.pubkey)).stage).to.equal(3n); // 3n is ACTIVATED
10511052
expect((await pdg.validatorStatus(sideValidator.container.pubkey)).stage).to.equal(3n); // 3n is ACTIVATED
@@ -1233,10 +1234,10 @@ describe("PredepositGuarantee.sol", () => {
12331234
).to.revertedWithCustomError(pdg, "WithdrawalCredentialsMatch");
12341235

12351236
// proving
1236-
await pdg.proveWC(validNotPredepostedValidatorWitness);
1237+
await pdg.proveWCAndActivate(validNotPredepostedValidatorWitness);
12371238
await expect(pdg.connect(vaultOperator).proveInvalidValidatorWC(validNotPredepostedValidatorWitness, validWC))
12381239
.to.revertedWithCustomError(pdg, "ValidatorNotPreDeposited")
1239-
.withArgs(validNotPredepostedValidator.container.pubkey, 2n);
1240+
.withArgs(validNotPredepostedValidator.container.pubkey, 3n); // 3n is ACTIVATED
12401241
});
12411242

12421243
it("reverts when trying to prove valid validator", async () => {
@@ -1413,7 +1414,7 @@ describe("PredepositGuarantee.sol", () => {
14131414
};
14141415

14151416
await expect(pdg.predeposit(stakingVault, [], [])).to.revertedWithCustomError(pdg, "ResumedExpected");
1416-
await expect(pdg.proveWC(witness)).to.revertedWithCustomError(pdg, "ResumedExpected");
1417+
await expect(pdg.proveWCAndActivate(witness)).to.revertedWithCustomError(pdg, "ResumedExpected");
14171418
await expect(pdg.activateValidator(witness.pubkey)).to.revertedWithCustomError(pdg, "ResumedExpected");
14181419
await expect(pdg.topUpExistingValidators([])).to.revertedWithCustomError(pdg, "ResumedExpected");
14191420
await expect(pdg.proveWCActivateAndTopUpValidators([], [])).to.revertedWithCustomError(pdg, "ResumedExpected");

0 commit comments

Comments
 (0)