Skip to main content

Overview

The following guide concerns performing the full withdrawal of a Stakehouse protocol validator including if the validator was created via LSD networks.

Step 1: Begin withdrawal on the consensus layer

For a validator to exit, it is mandatory to first broadcast an exit message (referred to as a voluntary withdrawal) on the consensus layer. It takes approximately 2 epochs to finalize the broadcast message. Once done, the consensus layer initiates the exit and starts processing final rewards for the validator. Once the rewards are processed, the last withdrawal is of 32 ETH, unless the validator is leaking, in which case it will be less than 32 ETH. All the withdrawal ETH is sent to the withdrawal address associated with the validator. For all the Stakehouse validators, the account manager contract address is the withdrawal address.

const broadcastVoluntaryWithdrawalTx = await sdk.withdrawal.broadcastVoluntaryWithdrawal(BEACON_NODE, blsKeystore, password);
console.log("tx: ", broadcastVoluntaryWithdrawalTx);

Step 2: Get the finalized epoch report

Finalised epoch report is the report for a validator received from the consensus layer. This report is necessary for the execution layer (the smart contracts) to identify a BLS public key and its status.

let finalisedReport = await sdk.balanceReport.getFinalisedEpochReport(BEACON_NODE, blsPublicKey);
console.log("finalisedReport: ", finalisedReport);

Step 3: Authenticate the finalized epoch report

let authenticatedReport = await sdk.balanceReport.authenticateReport(BEACON_NODE, finalisedReport);
console.log("authenticatedReport: ", authenticatedReport);

Step 4: Report withdrawal

Report voluntary withdrawal to the Stakehouse contracts on the execution layer so the protocol knows the validator’s intent to exit.

await sdk.withdrawal.reportVoluntaryWithdrawal(
stakehouseAddress,
authenticateReportResult
)

Step 5: Get the validator index

const validatorIndex = finalisedReport.validatorIndex;
console.log("validatorIndex: ", validatorIndex);

Step 6: Get total ETH sent to BLS public key

Total ETH is the amount of ETH deposited for the BLS public key on the consensus layer.

const totalETHSentToBLSPublicKey = await sdk.balanceReport.getTotalETHSentToBlsKey(blsPublicKey);
console.log("totalETHSentToBLSPublicKey: ", totalETHSentToBLSPublicKey.toString());

Step 7: Get SLOT indexes

To report all the withdrawals to the contract, it is necessary to get the first and last SLOT index of the validator. Using this, the user can further query all the sweep reports and report them to the contracts to maximize ETH rewards.

const slotIndexes = await sdk.balanceReport.getStartAndEndSlotByValidatorIndex(
validatorIndex
);
const firstSlotIndex = slotIndexes[1];
const lastSlotIndex = slotIndexes[0];

Step 8: Get all the dETH sweeps

Now that the first and last SLOT index is known, use it to query all the dETH sweeps.

let sweeps = await sdk.balanceReport.getDETHSweeps(
validatorIndex,
firstSlotIndex,
lastSlotIndex
);
console.log("dETHSweeps: ", sweeps);

Step 9: Get the final sweep

The final sweep is the last withdrawal for the BLS public key which amounts to 32 ETH (if the validator was leaking or slashed, then the final withdrawal will be less than 32 ETH).

let finalSweep = sdk.balanceReport.getFinalSweep(
BEACON_NODE,
validatorIndex
);

Step 10: Formatting and filtering dETH sweeps

The dETH sweeps received from the previous step contains the final sweep. The deposit router and the contracts would reject these sweep reports if it contains the final sweep. Hence, it is necessary to filter it out.

const filteredInsideSweeps = sweeps.sweeps.filter(
(sweep: any) =>
sweep.withdrawal_index &&
Number(sweep.withdrawal_index) !== Number(finalSweep.sweep.index)
)

sweeps = { sweeps: filteredInsideSweeps }

The sweeps received previously may also have some sweeps which have already been reported. So, it is important to only include the unreported sweeps.

let unreportedSweeps = await sdk.withdrawal.filterUnreportedSweepReports(sweeps.sweeps)
sweeps = { sweeps: unreportedSweeps }

Step 11: Calculate sum of all the dETH sweeps

const sumOfSweeps = sdk.balanceReport.calculateSumOfSweeps(sweeps.sweeps);
console.log("sumOfSweeps: ", sumOfSweeps.toString());

Step 12: Verify and report sweeps

The contract needs the sweep reports to be verified. Hence, all the sweeps need to be sent to the deposit router for verification. The deposit router will generate a signature valid for 20 minutes. This signature will be attached along with the sweep reports. The verified report needs to be formatted and then sent to the contracts.

if (!sumOfSweeps.eq(ethers.BigNumber.from('0'))) {
const verifyAndReport = await sdk.withdrawal.verifyAndReportAllSweepsAtOnce(
stakeHouse,
totalETHSentToBLSPublicKey.toString(),
sweeps.sweeps,
finalisedReport,
true
)

listOfUnverifiedReports = verifyAndReport.listOfUnverifiedReports
}

Step 13: Calculate sum of unverified sweeps

The `verifyAndReportAllSweepsAtOnce` function returns the transaction hash of the sweeps reported to the contract as well as a list of all the sweep reports which failed to be verified so that the user can retry to verify them.

const sumOfUnVerifiedReports = sdk.balanceReport.calculateSumOfSweeps(
listOfUnverifiedReports
);

Step 14: Generate final report

The contracts require some additional validator data along with the sweeps. To get this data, `generateFinalReport` function can be used.

let finalReport = await sdk.balanceReport.generateFinalReport(
BEACON_NODE_URL,
withdrawValidatorId,
totalETHSentToBLSPublicKey,
sumOfUnVerifiedReports,
listOfUnverifiedReports,
finalSweep
)

Step 15: Formatting final report for verification

The final report needs to be verified by the deposit router similar to all sweep reports.

finalReport.blsPublicKey = sdk.utils.remove0x(finalReport.blsPublicKey)
finalReport.totalETHSentToBLSKey = totalETHSentToBLSPublicKey.toString()
finalReport.sumOfUnreportedSweeps = sumOfUnVerifiedReports.toString()

Step 16: Verify final report

The final report generated needs to be verified by the deposit router before reporting it to the contracts.

let verifyFinalReport
verifyFinalReport = await sdk.balanceReport.verifyFinalReport(
finalReport
)

Step 17: Submit final report and withdrawa

This is the step in the withdrawal process where ETH is claimed from the Stakehouse protocol. Where the validator was created at the base protocol by a solo staker, they will receive the principle plus rewards assuming there was no slashing. If the validator was created using LSD network, the rage quit assistant will receive the ETH so that it can distribute the ETH correctly to the liquidity providers of the specific validator that exited.

The final report (for the last withdrawal) is reported to the smart contracts and the unstaking is trigerred, both in the same transaction.

This step will be different for LSD validators and solo-stakers.
a) For solo-stakers:

const reportAndUnstakeTx = await sdk.withdrawal.reportFinalSweepAndWithdraw(
finalReport.totalETHSentToBLSPublicKey,
finalReport.unreportedSweeps,
verifyFinalReport
)
console.log("reportAndUnstakeTx: ", reportAndUnstakeTx)

b) For LSD validators:

const reportAndUnstakeTx = await sdk.wizard.executeFullWithdrawalInRageQuitAssistant(
rageQuitAssistantAddress,
finalReport.totalETHSentToBLSPublicKey,
finalReport.unreportedSweeps,
verifyFinalReport
)
console.log("reportAndUnstakeTx: ", reportAndUnstakeTx)

Claiming ETH as a liquididty provider of an LSD network

The flow for claiming your share of the unstaked ETH of an LSD Stakehouse validator will differ depending on whether you are: a node runner, a protected staking liquidity provider or a fees and mev liquidity provider. It is possible to be a liquidity provider for more than one category.

Note: When it comes to being a liquidity provider for protected staking and or fees and mev, some users opt to supply liquidity to the giant pools which is a network agnostic way of offering liquidity where as fren delegation is being a liquidity provider for a specific validator in a specific LSD network.

Claiming ETH as a protected staking liquidity provider

Fren Delegation claim

A user that supplied liquidity to protected staking via fren delegation would have received dstETH_ token in their wallet - up to 24 dstETH_ is minted for a specific validator where each validator in an LSD network will have its own dstETH_ token.

Users that are holding dstETH_ token for a rage quit validator need to interact with the protected staking pool of their validator to claim the unstaked ETH.

One can use the subgraph to fetch the protected staking pool of a validator:

{
lsdvalidators(where: {
id: "0xaaa933a0c6e7200a64f2e71c53587f5ce3bee3a8b42fba1c0b20220a57a118ffec27990de842e067bfd168dfe33e0f9f"
}) {
smartWallet {
liquidStakingNetwork {
savETHPool
}
}
}
lptokens(where: {
blsPublicKey: "0xaaa933a0c6e7200a64f2e71c53587f5ce3bee3a8b42fba1c0b20220a57a118ffec27990de842e067bfd168dfe33e0f9f"
}) {
id
tokenType
}
}

savETHPool is the protected staking pool and the LP token query is important for fetching the address of the PROTECTED_STAKING_LP token which is the dstETH_ token we mentioned earlier.

We will use this part of the SDK to execute the claim transaction: https://docs.joinstakehouse.com/lsd/WizardSaveth/

Now, with the wizard SDK we can execute the following:

const tx = await wizard.savETHPool.batchClaimETHFromRageQuit(
[lpTokenAddress],
[userBalance]
)

when claiming from the giant pool the function is simplier:

batchClaimETHFromRageQuit(blsPublicKeyArray)

Claiming ETH as a fees and mev liquidity provider

Fren Delegation claim

A user that supplied liquidity to the fees and mev pool via fren delegation would have received ETHLP_ token in their wallet - up to 4 ETHLP_ is minted for a specific validator where each validator in an LSD network will have its own ETHLP_ token.

Users that are holding ETHLP_ token for a rage quit validator need to interact with the fees and mev pool of their validator to claim the unstaked ETH.

One can use the subgraph to fetch the fees and mev pool of a validator:

{
lsdvalidators(where: {
id: "0xaaa933a0c6e7200a64f2e71c53587f5ce3bee3a8b42fba1c0b20220a57a118ffec27990de842e067bfd168dfe33e0f9f"
}) {
smartWallet {
liquidStakingNetwork {
feesAndMevPool
}
}
}
lptokens(where: {
blsPublicKey: "0xaaa933a0c6e7200a64f2e71c53587f5ce3bee3a8b42fba1c0b20220a57a118ffec27990de842e067bfd168dfe33e0f9f"
}) {
id
tokenType
}
}

feesAndMevPool is the fees and mev staking pool and the LP token query is important for fetching the address of the FEES_AND_MEV_LP token which is the ETHLP_ token we mentioned earlier.

We will use this part of the SDK to execute the claim transaction: https://docs.joinstakehouse.com/lsd/WizardFeesandmev/

Now, with the wizard SDK we can execute the following:

const tx = await wizard.feesAndMevPool.batchClaimETHFromRageQuit(
[lpTokenAddress],
[userBalance]
)

when claiming from the giant pool the function is simplier:

batchClaimETHFromRageQuit(blsPublicKeyArray)

Claiming ETH as a node operator

As a node operator and assuming no slashing, the 4 ETH deposited can be claimed directly from the rage quit assistant which can be looked up with the following subgraph query:

{
lsdrageQuitAssistants(where: {
blsPublicKey: "0xaaa933a0c6e7200a64f2e71c53587f5ce3bee3a8b42fba1c0b20220a57a118ffec27990de842e067bfd168dfe33e0f9f"
}) {
id
}
}

then we need to use the following utility to instantiate an instance of the rage quit assistant contract: https://docs.joinstakehouse.com/lsd/WizardContract/

once done the claim is as simple as:

const tx = await contract.nodeOperatorClaim()

Anyone can execute but the ETH will always go to the node operator.