# Sidecar Staker Shares Calculation
# Background
The staker shares state model is the heart of the rewards calculation. It calculates every individual share delta, which are then summed by [stakerShareSnapshots](https://github.com/Layr-Labs/sidecar/blob/v3.0.0-rc.4/pkg/rewards/stakerShareSnapshots.go) to find the current shares at a given snapshot.
# Overview
Note that the primary difference between the below two equations is that NativeETHShares also have BeaconChainSlashings.
## ERC-20 Shares
The shares for a staker, $s$, and strategy, $y$, up to some timestamp $t$:
Shares$_{s,y,t}$ = $\sum_{i=0}^{t}$ Deposits$_{s,y,i}$ $-$ M1Withdrawals$_{s,y,i}$ $-$ M2Withdrawals$_{s,y,i}$ $-$ SlashingWithdrawals$_{s,y,i}$ $-$ AVSSlashings$_{s,y,i}$
## Beacon Chain Shares
The BeaconChain (native eth) shares for a staker is given by:
BeaconShares$_{s,t}$ = $\sum_{i=0}^{t}$ PodSharesUpdated$_{s,i}$ $-$ M1Withdrawals$_{s,i}$ $-$ M2Withdrawals$_{s,i}$ $-$ SlashingWithdrawals$_{s,i}$ $-$ BeaconChainSlashings$_{s,i}$ $-$ AVSSlashings$_{s,i}$
# Events
## Deposits
### ERC-20 Deposits
Every time an ERC-20 is deposited in the protocol, we [emit]() a `Deposit` event from the `StrategyManager`. Completing withdrawals as *shares* also emits this event.
```solidity=
/**
* @notice Emitted when a new deposit occurs on behalf of `staker`.
* @param staker Is the staker who is depositing funds into EigenLayer.
* @param strategy Is the strategy that `staker` has deposited into.
* @param token Is the token that `staker` deposited.
* @param shares Is the number of new shares `staker` has been granted in `strategy`.
*/
event Deposit(address staker, IERC20 token, IStrategy strategy, uint256 shares);
```
### Beacon Chain Deposits
Any time a checkpoint or credential proof is submitted wiht a *positive* share delta, we emit `PodSharesUpdated` from the `EigenPodManager`. This event is emitted when the `_addShares` function is called, either when completing a withdrawal as shares or checkpointing a positive balance.
```solidity=
/// @notice Emitted when the balance of an EigenPod is updated
event PodSharesUpdated(address indexed podOwner, int256 sharesDelta);
```
The previous event model emitted this event for beacon chain increases & decreases (slashings). Negative deltas are now handled by the `BeaconChainSlashing` event.
The above event is also emitted when a user has a negative share deficit (from the M2 system) and completes a withdrawal. In this case, the user has to "pay-off" its debt before withdrawing the rest.
## Withdrawals
There have been 3 types of withdrawals in the EigenLayer protocol. Only a single withdrawal type has been present at a given time.
### M1 Withdrawals
The M1 withdrawal that was emitted from the `StrategyManager`. Upon upgrade to M2, these withdrawals were migrated to the `DelegationManager`.
Withdrawals that were queued in M1, but not completed, were migrated to M2 have an M1 and M2 withdrawal event. In order to de-dupe, the `stakerShares` state model [uses](https://github.com/Layr-Labs/sidecar/blob/87216494665520ef9ff17f72d7052713d41c5a67/pkg/eigenState/stakerShares/stakerShares.go#L587) the [handleMigratedM2StakerWithdrawals](https://github.com/Layr-Labs/sidecar/blob/87216494665520ef9ff17f72d7052713d41c5a67/pkg/eigenState/stakerShares/stakerShares.go#L266) to only count the M1 withdrawal.
```solidity=
/**
* @notice Emitted when a new withdrawal occurs on behalf of `depositor`.
* @param depositor Is the staker who is queuing a withdrawal from EigenLayer.
* @param nonce Is the withdrawal's unique identifier (to the depositor).
* @param strategy Is the strategy that `depositor` has queued to withdraw from.
* @param shares Is the number of shares `depositor` has queued to withdraw.
*/
event ShareWithdrawalQueued(
address depositor, uint96 nonce, IStrategy strategy, uint256 shares
);
/// @notice Emitted when a queued withdrawal is *migrated* from the StrategyManager to the DelegationManager
event WithdrawalMigrated(bytes32 oldWithdrawalRoot, bytes32 newWithdrawalRoot);
```
### M2 Withdrawals
M2 withdrawals are emitted from the `DelegationManager`.
```solidity=
/**
* @notice Emitted when a new withdrawal is queued.
* @param withdrawalRoot Is the hash of the `withdrawal`.
* @param withdrawal Is the withdrawal itself.
*/
event WithdrawalQueued(bytes32 withdrawalRoot, Withdrawal withdrawal);
/**
* Struct type used to specify an existing queued withdrawal. Rather than storing the entire struct, only a hash is stored.
* In functions that operate on existing queued withdrawals -- e.g. completeQueuedWithdrawal`, the data is resubmitted and the hash of the submitted
* data is computed by `calculateWithdrawalRoot` and checked against the stored hash in order to confirm the integrity of the submitted data.
*/
struct Withdrawal {
// The address that originated the Withdrawal
address staker;
// The address that the staker was delegated to at the time that the Withdrawal was created
address delegatedTo;
// The address that can complete the Withdrawal + will receive funds when completing the withdrawal
address withdrawer;
// Nonce used to guarantee that otherwise identical withdrawals have unique hashes
uint256 nonce;
// Block number when the Withdrawal was created
uint32 startBlock;
// Array of strategies that the Withdrawal contains
IStrategy[] strategies;
// Array containing the amount of shares in each Strategy in the `strategies` array
uint256[] shares;
}
```
### Slashing Withdrawals
After the slashing upgrade, all withdrawals will follow the below event. There is no migration between M2 withdrawals and slashing withdrawals.
We specifically decrement the `sharesToWithdraw` parameter. `scaledShares` are only relevant for calculating what the user would receive upon *completion* of the withdrawal, hence it can be discarded.
```solidity=
/**
* @notice Emitted when a new withdrawal is queued.
* @param withdrawalRoot Is the hash of the `withdrawal`.
* @param withdrawal Is the withdrawal itself.
* @param sharesToWithdraw Is an array of the expected shares that were queued for withdrawal corresponding to the strategies in the `withdrawal`.
*/
event SlashingWithdrawalQueued(bytes32 withdrawalRoot, Withdrawal withdrawal, uint256[] sharesToWithdraw);
/**
* @dev A struct representing an existing queued withdrawal. After the withdrawal delay has elapsed, this withdrawal can be completed via `completeQueuedWithdrawal`.
* A `Withdrawal` is created by the `DelegationManager` when `queueWithdrawals` is called. The `withdrawalRoots` hashes returned by `queueWithdrawals` can be used
* to fetch the corresponding `Withdrawal` from storage (via `getQueuedWithdrawal`).
*
* @param staker The address that queued the withdrawal
* @param delegatedTo The address that the staker was delegated to at the time the withdrawal was queued. Used to determine if additional slashing occurred before
* this withdrawal became completeable.
* @param withdrawer The address that will call the contract to complete the withdrawal. Note that this will always equal `staker`; alternate withdrawers are not
* supported at this time.
* @param nonce The staker's `cumulativeWithdrawalsQueued` at time of queuing. Used to ensure withdrawals have unique hashes.
* @param startBlock The block number when the withdrawal was queued.
* @param strategies The strategies requested for withdrawal when the withdrawal was queued
* @param scaledShares The staker's deposit shares requested for withdrawal, scaled by the staker's `depositScalingFactor`. Upon completion, these will be
* scaled by the appropriate slashing factor as of the withdrawal's completable block. The result is what is actually withdrawable.
*/
struct Withdrawal {
address staker;
address delegatedTo;
address withdrawer;
uint256 nonce;
uint32 startBlock;
IStrategy[] strategies;
uint256[] scaledShares;
}
```
## Slashings
### AVS Slashings
When an operator is slashed, shares must be decremented from all stakers delegated to the operator at the time of the slash. The `OperatorSlashed` event in the `AllocationManager` keeps track of this.
```solidity=
/// @notice Emitted when an operator is slashed by an operator set for a strategy
/// `wadSlashed` is the proportion of the operator's total delegated stake that was slashed
event OperatorSlashed(
address operator, OperatorSet operatorSet, IStrategy[] strategies, uint256[] wadSlashed, string description
);
```
### Beacon Chain Slashings
When a staker is slashed on the beacon chain, the `EigenPodManager` emits the following event:
```solidity=
/// @notice Emitted when a staker's beaconChainSlashingFactor is updated
event BeaconChainSlashingFactorDecreased(
address staker, uint64 prevBeaconChainSlashingFactor, uint64 newBeaconChainSlashingFactor
);
```
Note that there is no `wadsSlashed` parameter here. We can compute this by taking the proportional between the new and previous slashing factors. The `handleBeaconChainSlashingFactorDecreasedEvent` handler does so with the following:
```go=
wadSlashed := big.NewInt(1e18)
wadSlashed = wadSlashed.Mul(wadSlashed, new(big.Int).SetUint64(outputData.NewBeaconChainSlashingFactor))
wadSlashed = wadSlashed.Div(wadSlashed, new(big.Int).SetUint64(outputData.PrevBeaconChainSlashingFactor))
wadSlashed = wadSlashed.Sub(big.NewInt(1e18), wadSlashed)
```
# Calculation
Event processing occurs by first handling all the relevant tx logs and then commiting the state diff on a per-block basis. The former processes events, while the latter does any additional computation prior to writing to the DB.
## `getStateTransitions`
Every state model has a `HandleStateChange` handler, which gets the state changes that have occurred
We start with [getStateTransitions](https://github.com/Layr-Labs/sidecar/blob/87216494665520ef9ff17f72d7052713d41c5a67/pkg/eigenState/stakerShares/stakerShares.go#L530), which parses each of the above events in their associated handlers.
The key part of this processing is appending every share delta and slashing into their respective accumulators:
```go=
ss.shareDeltaAccumulator[log.BlockNumber] = append(ss.shareDeltaAccumulator[log.BlockNumber], shareDeltaRecords...)
ss.slashingAccumulator[log.BlockNumber] = append(ss.slashingAccumulator[log.BlockNumber], slashDiffs...)
```
These accumulators are then used inside the `prepareState` function, where slashings are applied to the shares of every staker.
## Processing
Slashing processing is done in the `prepareState` function, which is called before the block is processed from `CommitFinalState`.
Let's walk through the code line by line.
We get the accumulators that were populated by handling the even updates for the given block. Accumulators are keyed based on the block number. Note that the contents in these accumulators are *ordered* since events are processed chronologically by log index.
```go=
_, ok := ss.shareDeltaAccumulator[blockNumber]
if !ok {
msg := "delta accumulator was not initialized"
ss.logger.Sugar().Errorw(msg, zap.Uint64("blockNumber", blockNumber))
return nil, errors.New(msg)
}
slashes, ok := ss.slashingAccumulator[blockNumber]
if !ok {
msg := "slashing accumulator was not initialized"
ss.logger.Sugar().Errorw(msg, zap.Uint64("blockNumber", blockNumber))
return nil, errors.New(msg)
}
records := make([]*StakerShareDeltas, 0)
netDeltas := make(map[string]*big.Int)
shareDeltaIndex := 0
slashingIndex := 0
```
Here, we loop through the accumulators as long as there is either a share delta or slashing to process.
```go=
for shareDeltaIndex < len(ss.shareDeltaAccumulator[blockNumber]) || slashingIndex < len(ss.slashingAccumulator[blockNumber]) {
```
Set the share and slash accumulators to the current log index. Note that these are initialized to `math.MaxUint64` so that if an accumulator was empty, it would be processed last.
```go=
// initialize to max logIndex so we can compare
shareDelta := &StakerShareDeltas{LogIndex: math.MaxUint64}
slashDiff := &SlashDiff{LogIndex: math.MaxUint64}
// load the accumulators if index exists
if shareDeltaIndex < len(ss.shareDeltaAccumulator[blockNumber]) {
shareDelta = ss.shareDeltaAccumulator[blockNumber][shareDeltaIndex]
}
if slashingIndex < len(ss.slashingAccumulator[blockNumber]) {
slashDiff = ss.slashingAccumulator[blockNumber][slashingIndex]
}
```
If the share delta (ie. deposit or withdrawal) occurred prior to the slash, then process the delta normally. We increment `netDeltas` if these future deposits/withdrawals have to be applied to slashings within the *same* block. These are keyed on a per staker-strategy basis.
```go=
if shareDelta.LogIndex < slashDiff.LogIndex {
key := fmt.Sprintf("%s-%s", shareDelta.Staker, shareDelta.Strategy)
ss.logger.Sugar().Debugw("regular share delta",
zap.String("staker", shareDelta.Staker),
zap.String("strategy", shareDelta.Strategy),
zap.String("shares", shareDelta.Shares),
)
// apply the shareDelta
if _, ok := netDeltas[key]; !ok {
netDeltas[key] = big.NewInt(0)
}
shares, success := numbers.NewBig257().SetString(shareDelta.Shares, 10)
if !success {
return nil, fmt.Errorf("failed to convert shares to big.Int: %s", shareDelta.Shares)
}
netDeltas[key] = netDeltas[key].Add(netDeltas[key], shares)
records = append(records, shareDelta)
shareDeltaIndex++
}
```
If the slashing occurred *after* the deposit or withdrawal, we need to get the latest shares and then apply the slash accordingly.
First, we log the slash event.
```go=
ss.logger.Sugar().Debugw("Slashing",
zap.String("slashedEntity", slashDiff.SlashedEntity),
zap.Bool("beaconChain", slashDiff.BeaconChain),
zap.String("strategy", slashDiff.Strategy),
zap.String("wadSlashed", slashDiff.WadSlashed.String()),
zap.String("transactionHash", slashDiff.TransactionHash),
zap.Uint64("logIndex", slashDiff.LogIndex),
)
var stakerShares *[]StakerShares
var err error
```
If the slash was an AVS slash (not a beacon chain slash), we only know the operator that was slashed. Thus, we must get all the stakers delegated to the operator at the time of the slash. `GetDelegatedStakerSharesAtTimeOfSlashing` gets all the stakers and their current shares delegated to the operator at the given block.
```go=
if !slashDiff.BeaconChain {
stakerShares, err = ss.GetDelegatedStakerSharesAtTimeOfSlashing(slashDiff)
}
```
If it's a beacon chain slash, simply get the staker's current shares using `GetStakerSharesFromDB`.
```go=
stakerShares, err = ss.GetStakerSharesFromDB(slashDiff.SlashedEntity, slashDiff.Strategy)
```
Loop through all the staker shares. First check if they have any deltas from previous deposits or withdrawals.
```go=
for _, stakerShare := range *stakerShares {
// loop through every delegated staker
// check if they have previous deposits, withdrawals, or slashes in the current block that need to be taken into account
key := fmt.Sprintf("%s-%s", stakerShare.Staker, slashDiff.Strategy)
_, relevantDeltasInCurrentBlock := netDeltas[key]
// if they have shares in the strategy being slashed before the current block
// or they have deltas for the strategy being slashed in the current block
```
Process the slashing if there will be a nonzero share decrement. THis is true if the staker had shares prior to the current block OR if the staker had deltas in the current block that need to be slashed. Before processin the slashing, add the current share sbefore slash with any deltas in the block.
```go=
if !strings.EqualFold(stakerShare.Shares, "0") || relevantDeltasInCurrentBlock {
if !success {
return nil, fmt.Errorf("failed to convert shares to big.Int: %s", stakerShare.Shares)
}
// add the net delta in this block
if !relevantDeltasInCurrentBlock {
netDeltas[key] = big.NewInt(0)
}
sharesBeforeSlash = sharesBeforeSlash.Add(sharesBeforeSlash, netDeltas[key])
```
Calculate the delta from slashing. Since `WadSlashed` is the proportion slashed in base 1e18, we multiple the shares by `WadSlashed` and divide by 1e18. 1e18 is negative since the slashing is a negative share delta.
```go=
// add a delta for the slashing
sharesSlashed := new(big.Int).Div(new(big.Int).Mul(sharesBeforeSlash, slashDiff.WadSlashed), big.NewInt(-1e18))
```
Add a delta to the records. Note that this does not need to be ordered since processing these records properly sums over the records in order of `logIndex` and `blockNumber`.
```go=
// add a delta for the slashing
sharesSlashed := new(big.Int).Div(new(big.Int).Mul(sharesBeforeSlash, slashDiff.WadSlashed), big.NewInt(-1e18))
records = append(records, &StakerShareDeltas{
Staker: stakerShare.Staker,
Strategy: slashDiff.Strategy,
Shares: sharesSlashed.String(),
StrategyIndex: 0,
TransactionHash: slashDiff.TransactionHash,
LogIndex: slashDiff.LogIndex,
BlockNumber: slashDiff.BlockNumber,
})
```
Finally, since the staker has been slashed, we must add this delta back to netDeltas in case further slashes are in the block.
```go=
// subtract (add the negative) the slashed shares from the net delta
netDeltas[key] = netDeltas[key].Add(netDeltas[key], sharesSlashed)
```
# Notes/Edge Cases
## Rounding
Our [slashing documentation](https://github.com/Layr-Labs/eigenlayer-contracts/tree/v1.1.0-testnet/docs/core/accounting) details rounding edge cases. In addition to these, the staker shares in the sidecar will not always be 1:1 with `getWithdrawableShares` on-chain. This is due to the difference in where the rounding occurs on and off-chain.
Let's take a practical example. Alice was slashed in the following [tx](https://holesky.etherscan.io/tx/0x2363dd9d5bb0ba16d87b12090a96c4e2bbfc7046e543539597349c93697d86ed). Alice had deposited `1087050406429303222` shares. Alice's operator, who had allocated 10% to the operatorSet is slashed 25%. Therefore, Alice's stake should be slashed by 2.5%.
On-chain, her [withdrawable shares](https://www.wolframalpha.com/input?i=1087050406429303222+*+%28975000000000000000+%2F+1e18%29) post-slash are now `1059874146268570641`, given by `depositShares * operatorMagnitude` (both deposit scaling factor & beacon chain slashing factor are WAD). The withdrawable share delta here is: $$1059874146268570641 - 1087050406429303222 = -27176260160732581$$
In the sidecar, we see `WadSlashed` as 2.5e16 or `25000000000000000`. The sidecar computes this as
$$1087050406429303222 * 25000000000000000 / -1e18 = -27176260160732580 $$
Note the 1 wei difference in share deltas. On-chain, the rounding error is applied to the final output when calculating the withdrawable shares. Off-chain, the rounding is applied to the intermediate calculation since we *round down the share delta*, which results in an additional 1 wei difference in end result.
## Slashing while in the Withdrawal Queue
Note that a staker can get slashed while they are in the withdrawal queue. We have two scenarios.
1. All funds are in the withdrawal queue
2. A portion of funds are in the withdrawal queue. A portion of funds are not.
In Case 1, if a staker is slashed, that will be applied once the staker *completes* the withdrawal. Since the staker has 0 shares once they enter the withdrawal queue, no share delta will be applied.
In Case 2, only the funds that are not in the withdrawal queue will have the slash applied to it.
## Delegations
As part of figuring out which staker's to slash for a given operator, we need to find out all stakers delegated to the operator at the time of the slash.
Since stakers may delegate to an operator and be slashed in the same block, we must get the list of staker delegations *prior* to applying the slashes. This is done via a precommit process in the sidecar pipeline. Here, we get all teh delegations and add it to the staker shares model state
```go=
// get the in-memory staker delegations for this block. If there arent any, theres nothing to do
delegations := stakerDelegationModel.GetAccumulatedState(blockNumber)
if len(delegations) == 0 {
sp.logger.Sugar().Debug("No staker delegations found for block number", zap.Uint64("blockNumber", blockNumber))
return nil
}
// inject the current block delegations into the staker shares model
precommitDelegations := make([]*stakerShares.PrecommitDelegatedStaker, 0)
for _, d := range delegations {
delegation := &stakerShares.PrecommitDelegatedStaker{
Staker: d.Staker,
Operator: d.Operator,
Delegated: d.Delegated,
TransactionHash: d.TransactionHash,
TransactionIndex: d.TransactionIndex,
LogIndex: d.LogIndex,
}
precommitDelegations = append(precommitDelegations, delegation)
}
stakerSharesModel.PrecommitDelegatedStakers[blockNumber] = precommitDelegations
```
Later, this mapping is used in the `GetMergedDelegatedStakerSharesAtTimeOfSlashing` function. Specifically, we:
1. Call `GetDelegatedStakerSharesAtTimeOfSlashing` which returns all the shares for stakers delegated to the operator in the previous block
2. Call `GetDelegatedStakerSharesInPrecommitState`, which returns the shares of stakers who delegated in the current block
- Call `getFlattenedPrecommitDelegatedStakers`, which returns delegations that happened in the current block, but prior to the slash
3. Combine the above two mappings to get the staker shares to apply slashings to