Native Restaking
Ethereum native restaking on Karak utilizes a continual balance snapshot system combined with an ERC4626 vault, where the users are allocated share tokens to handle their stake. The users can restake the Ethereum staked on their Beacon Chain validators to Karak Operators, which serve as validators for the Karak Distributed Secure Services (DSS). Users will continue to earn yields from their beacon chain validators, which are also immediately restakable to the respective Karak Operators. Additionally, they will earn rewards from the Operators they choose to restake into proportional to the restaked amount.
Terminologies and Architecture
Karak Native Restaking builds upon the existing Karak Core standards, which enables easy deployment of the vault handling native restaking. The architecture comprises a NativeVault
which is an ERC4626 vault with overridden functions to handle shares based on a virtual asset instead of an ERC20 token. The NativeVault
is deployed and managed by Core and is always in a 1:1 mapping with an Operator. According to the Karak Core concepts this implies that a vault has been delegated to the Operator.
A NativeVault
consists of a createNode
function which deploys a new NativeNode
for the caller. A NativeNode
is deployed at a CREATE2 address which takes in both the Operator and the function caller as the salt, hence enforcing a single NativeNode
per user per operator. The details of a NativeNode
is stored as an owner => NativeNode
mapping in the NativeVault
leaving the NativeNode
as a simple holder contract with just a withdrawal function and NativeVault
serving as the Beacon for the contract implementation.
A NativeNode
can support multiple validators by calling a NativeVault
function to prove that the validator’s withdrawal credential is pointed to the owner’s NativeNode
i.e. - validateWithdrawalCredentials
. Hence, all the Ethereum yields coming from the validator will be accrued at the NativeNode
and the owner will be awarded shares for it. The shares are then delegated to the Operator of the NativeVault
and are subject to slashing. The share price mathematics remains the same as a standard ERC4626 vault.
The NativeVault
is initialized with an address where it is supposed to send all the slashed ETH called the slashStore
. This is the common address to accumulate all slashed assets coming from different operators such that it is easy for the slashing handler to decide what to do with those assets. How the slashStore
accumulates slashed ETH is discussed later in the document.
With all the terminologies explained the following architecture diagram should be legible –
Deploying a NativeNode
The NativeVault
provides a function to deploy a new NativeNode
using:
function `createNode`() external;
This deploys a new NativeNode
with msg.sender
as the owner of that node.
- The user then needs to set the address of the
NativeNode
contract as the withdrawal credentials for the validators they wish to restake. - Consequently, the user has to register Beacon Chain validators into it to be awarded shares for their staked ETH.
For that the node owner needs to call the following function in the INativeVault
interface:
function `validateWithdrawalCredentials`(
BeaconProofs.BeaconStateRootProof calldata beaconStateRootProof,
BeaconProofs.ValidatorFieldsProof[] calldata validatorFieldProofs
) external;
The proofs can be generated using the Karak CLI. Once successful, this function will mark these validators as ACTIVE
and increase the activeValidatorCount
for the node.
Creating Snapshots
Karak native restaking utilizes a balance snapshot system to keep the NativeNode
aware of the balance changes on the beacon chain.
NativeVault
updates the node owner's shares based on the balance changes on their NativeNode
and the beacon chain balance of the validators, which in turn changes the amount the node owner has staked into an Operator, affecting the rewards given to them. A user can create multiple native nodes staked into different Karak Operators by calling the createNode
function in the NativeVault
that is delegated to the Operator they would like to stake into. They have to handle all their native nodes individually by submitting periodic snapshots and can withdraw ETH from each node independently.
In a scenario where a native node’s snapshot has not been updated for a given time period (currently 7 days) the last snapshot is marked expired. In this case, anyone can start the snapshot on behalf of the owner.
The snapshot (balance update) merkle proof can be generated using the Karak CLI. The following flow diagram should capture the basic snapshot flow:
The following steps need to be performed to finish a new snapshot:
- Start a new snapshot using this function –
function startSnapshot() external
. If there is no balance change from the last snapshot state, then the function will revert with aNoBalanceUpdateToSnapshot
error. - Submit snapshot (balance) proofs for all active validators in the
NativeNode
using this function –
function validateSnapshotProofs(
address nodeOwner,
BeaconProofs.BalanceProof[] calldata balanceProofs,
BeaconProofs.BalanceContainer calldata balanceContainer
) external;
- Once a snapshot proof has been submitted, the snapshot will automatically be finalized and emit a
SnapshotFinished
event.
The native vault's totalAssets
is a uint256
which is updated based on this equation –
Based on this the equivalent shares are awarded to the node owner. This process can be outsourced to a cron job or keeper service to regularly update restaked ETH.
Withdrawing from the NativeNode
The validators registered with a NativeNode
have their withdrawal credentials pointed to the respective nodes. Therefore, all the partial withdrawals coming from the beacon chain are accrued at the NativeNode
.
Node owners have to go through a withdrawal queue to withdraw their accumulated ETH from their NativeNode
. This withdrawal queue is meant to prevent a bad actor from indulging in slashable behavior and immediately withdrawing their funds. After starting a withdrawal the node owner is allowed to finish the withdrawal only after the MIN_WITHDRAW_DELAY
has passed to prevent escaping slashable behavior. The withdrawal can be started and finished using these functions in the NativeVault
:
function startWithdrawal(address to, uint256 weiAmount) external returns (bytes32 withdrawalKey);
function finishWithdrawal(bytes32 withdrawalKey) external;
However, not all ETH accumulated at the NativeNode
is withdrawable by the node owner since it depends on the share token value of the node owner. We will look at how slashing works in the following sections.
Full Withdrawals
In case a user chooses to withdraw their validator fully, all the ETH coming to the NativeNode
can be withdrawn using the same function as above. When the next snapshot is submitted and the beacon balance is 0 the validator status in the NativeNode
is marked as WITHDRAWN
. Consequently, that validator does not need a snapshot proof to be submitted for finishing snapshots.
Slashing and SlashStore
When an operator that has delegated native staked ETH is slashed, the totalAssets
in the NativeVault is decreased by the slashing amount (emulating assets being transferred out of the vault to the slashingHandler). This emulation behavior is necessary since at the point of slashing, all the slashed ETH need not be available at the nodes. Additionally, transferring slashed assets from all nodes will turn out to be an O(n) operation.
Instead, whenever a new snapshot is started, the slashed assets are calculated using this equation –
This is still not all the available assets that can be withdrawn at the time, hence the sweepable ETH is calculated as – min(node.balance, slashedAssets)
Then, the sweepable ETH is withdrawn to the slashStore
from where it is up to the slashing handler on what should be done with the slashed ETH.