owned this note
owned this note
Published
Linked with GitHub
# eODS - from a developer's point of view
eODS proposes a way to have delegations natively --in protocol-- without causing too much disturbance in the consensus layer.
As we moved forward with the specs we've taken into consideration:
1. The cost/benefit ratio of every decision taken.
2. The overhead this will bring to the system.
3. The safety implications of such a feature.
4. The UX improvements this will introduce.
I was quite adamant with number 4. I wanted ANYBODY with a wallet and some capital to be able to delegate -- start contributing to the protocol's financial safety, and of course generate some rewards.
Delegating should be intuitive, simple, safe and fast. We built the proposal on top of those pillars.
## Prerequisites to becoming a Delegator
There should be as little friction as possible between wanting to delegate any amount and actually doing it. No advanced knowledge on how a node is operated, no special CLI's or tools or processes.
All you should need is:
1. A wallet / execution address.
2. An amount you want to delegate.
## The nutshell
### Changes
We added as little things as possible in the existing EL / CL code.

#### Execution Layer:
1. Added a `DelegationOperationsContract`. This contract will handle most of the interactions between an execution address and the delegation operations (Delegate, Undelegate, etc).
2. Added a `DepositToDelegateContract`. This contract will handle moving ETH from EL to CL. It will follow the logic and implementation of THE `DepositContract`.
#### Consensus Layer:
1. Added the `Delegator` as an entity. We needed this to store the `execution_address` of a delegator and so to be able to track him in the delegation flow.
2. Added the `DelegatedValidator` as a wrapper around the existing Validator - to avoid messing with the existing code too much. This holds the amounts delegated for a specific `Validator`. Many delegators to one Validator wrapped in one entity.
3. Added the `BeaconChainAccounting` widget - a set of methods that are used to keep track of delegated amounts and is also used to settle balances when needed.
Of course we had to modify the `BeaconState` container, delegation adds new queues, and some methods in the Consensus Layer have also been modified to accommodate the new features.
> Delegating is not a mandatory action. For any existing validator, nothing will change in its normal way of operation unless the node operator decides to enable receiving delegations.
### Delegations - TL/DR

A naive and quick way of explaining delegations would be to say that each execution address that wants to delegate will be assigned a Delegator in CL. That will act like an account where undelegated Gwei will reside. That account can be topped up, withdrawn from, or certain amounts can be delegated/undelegated/redelegated.
Delegating/undelegating/redelegating implies moving an amount of Gwei between a Delegator's non delegated balance and a Validator's effective balance.
#### 1. Deposit to delegate
Using the `DepositToDelegate` contract, deposit some amount into the Delegator.
#### 2. Delegate
Using the Delegation Operations Contract, delegate an amount to a Validator. This will increase the Validator's effective balance (and its weight).

#### 3. Undelegate
Using the Delegation Operations Contract, undelegate some amount from a Validator. This will decrease the Validator's effective_balance and reduce its weight.
#### 4. Withdraw from delegator
Using the Delegation Operations Contract, withdraw some amount from the undelegated balance, and move it to the EL into a wallet.
## Delegation Operations Contract
Based on [EIP-7685: General purpose execution layer requests](https://eips.ethereum.org/EIPS/eip-7685) this contract allows an Execution Address to interface with delegations, without the need for a CLI or other tools/flows.

This contract will be executed when a delegation interaction is desired.
It supports different actions:
- `ACTIVATE_OPERATOR`
- used to activate an operator (allow an existing Validator to accept delegations)
- `DELEGATE`
- used to delegate an amount (move an amount from the non delegated balance of a Delegator to the delegated balance of a Validator)
- `UNDELEGATE`
- used to undelegate an amount (move an amount from the delegated balance of a Validator to the non delegated balance of a Delegator)
- `REDELEGATE`
- used to redelegate an amount (move an amount from an Validator's delegated balance to another's Validator's delegated balance)
- `WITHDRAW_FROM_DELEGATOR`
- used to withdraw an amount (move it from the non delegated balance of a Delegator to a wallet - EL)
Every time the contract is invoked it will generate a `DelegationOperationRequest` that is sent to CL via the `ExecutionRequests`, in a list of `DelegationOperationRequest`.
```python
class ExecutionRequests(Container):
deposits: List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]
withdrawals: List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD]
consolidations: List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD]
delegation_operations: List[DelegationOperationRequest, MAX_DELEGATION_OPERATIONS_REQUESTS_PER_PAYLOAD] # <== here
```
```python
class DelegationOperationRequest(Container):
type: Bytes1
source_pubkey: BLSPubkey
target_pubkey: BLSPubkey
amount: Gwei
execution_address: ExecutionAddress
```
The `DelegationOperationRequest` is a generic data wrapper used only to transport data from EL to CL. The data is unbundled and parsed in the consensus layer using the `type` parameter present in each `DelegationOperationRequest`.
For example, if `type=UNDELEGATE_REQUEST_TYPE`, the parser will create a `PendingUndelegateRequest`, fill the data it needs from the `DelegationOperationRequest`, and queue it in the state.
```python
class PendingUndelegateRequest(Container):
execution_address: ExecutionAddress
validator_pubkey: BLSPubkey
amount: Gwei
```
The method that handles the unbundling is:
```python
def process_delegation_operation_request(state: BeaconState,
delegation_operation_request: DelegationOperationRequest) -> None:
if delegation_operation_request.type == ACTIVATE_OPERATOR_REQUEST_TYPE:
state.pending_operator_activations.append(PendingActivateOperator(
validator_pubkey=delegation_operation_request.target_pubkey,
execution_address=delegation_operation_request.execution_address,
fee_quotient=delegation_operation_request.fee_quotient
))
elif delegation_operation_request.type == DEPOSIT_TO_DELEGATE_REQUEST_TYPE:
state.pending_deposits_to_delegate.append(PendingDepositToDelegate(
execution_address=delegation_operation_request.execution_address,
amount=delegation_operation_request.amount
))
elif delegation_operation_request.type == DELEGATE_REQUEST_TYPE:
state.pending_delegations.append(PendingDelegateRequest(
validator_pubkey=delegation_operation_request.target_pubkey,
execution_address=delegation_operation_request.execution_address,
amount=delegation_operation_request.amount,
slot=state.slot
))
... etc, handles more types
```
This method ONLY converts a `DelegationOperationRequest` to its proper CL counterpart, and queues that into its own buffer type in the state.
#### To sum up:
- an `execution address` via a contract sends a bunch of `DelegationOperationRequests` -- with different types -- in `execution_requests.delegation_operations`
- the `delegation_operations` is parsed in its entirety, for each block by `process_delegation_operation_request`
- each parsed request goes into the state, in its buffer. It waits there to be picked up and used later.
The buffers from the state accumulate and drain in a predictable manner, using the timing described below:
#### Timing:
- Adding to buffers
- ACTIVATE_OPERATOR: `on block`
- DELEGATE: `on block`
- UNDELEGATE: `on block`
- REDELEGATE: `on block`
- DEPOSIT_TO_DELEGATE: `on block`
- WITHDRAW_FROM_DELEGATOR: `on block`
- Draining from buffers
- ACTIVATE_OPERATOR: `on epoch`
- DELEGATE: `on epoch`
- UNDELEGATE: `on epoch`
- REDELEGATE: `on epoch`
- DEPOSIT_TO_DELEGATE: `on epoch`
- WITHDRAW_FROM_DELEGATOR: `on block` <== !!
With **one exception**, on each block we queue up requests in the buffers, and on each epoch we consume the buffers (practically emptying them). The how and why will be explained below.
## Containers
eODS adds a few containers and modifies a few containers. Most noteworthy are:
### Modified BeaconState
```python
class BeaconState(Container):
# [...BeaconState]
# The unique index used for withdrawals
next_withdrawal_from_delegators_index: WithdrawalIndex
# The list of Delegators
delegators: List[Delegator, DELEGATOR_REGISTRY_LIMIT]
# The list of non delegated balances. The index is parallel with `delegators`
delegators_balances: List[Gwei, DELEGATOR_REGISTRY_LIMIT]
# The list of DelegatedValidators, the wrappers on top of Validators that enable delegations
delegated_validators: List[DelegatedValidator, VALIDATOR_REGISTRY_LIMIT]
# The Delegation operations requests buffers
pending_operator_activations: List[PendingActivateOperator, PENDING_DELEGATION_OPERATIONS_LIMIT]
pending_deposits_to_delegate: List[PendingDepositToDelegate, PENDING_DELEGATION_OPERATIONS_LIMIT]
pending_delegations: List[PendingDelegateRequest, PENDING_DELEGATION_OPERATIONS_LIMIT]
pending_undelegations: List[PendingUndelegateRequest, PENDING_DELEGATION_OPERATIONS_LIMIT]
pending_redelegations: List[PendingRedelegateRequest, PENDING_DELEGATION_OPERATIONS_LIMIT]
pending_withdrawals_from_delegators: List[PendingWithdrawFromDelegatorRequest, PENDING_DELEGATION_OPERATIONS_LIMIT]
# Delegation exit queue
delegation_exit_queue: List[DelegationExitItem, PENDING_DELEGATION_OPERATIONS_LIMIT]
```
### Modified Validator
```python
class Validator(Container):
# [...Validator]
# This is a flag used to determine if a Validator accepts delegations
is_operator: boolean
```
### Added Delegator
```python
class Delegator(Container):
# The execution address bound to this Delegator
execution_address: ExecutionAddress
# The epoch in which this Delegator was activated. Used to calculate the withdrawal cooldown
delegator_entry_epoch: Epoch
```
### Added DelegatedValidator
In order to avoid excessive changes to Validator, the big changes have been added to a wrapper container. Here we store the delegated balances and quotas.
```python
class DelegatedValidator(Container):
# The Validator wrapped by this DelegatedValidator
validator: Validator
# The quota of the validator's own balance, from the total balance (own + delegations)
delegated_validator_quota: uint64
# A list of quotas for each delegator that delegated an amount here. This list is parallel with state.delegators
delegators_quotas: List[Quota, DELEGATOR_REGISTRY_LIMIT]
# A list of amounts for each delegator that delegated an amount here. This list is parallel with state.delegators
delegated_balances: List[Gwei, DELEGATOR_REGISTRY_LIMIT]
# The sum of all entries in delegated_balances. Used as a performance improvement - so we don't always sum the list
total_delegated_balance: Gwei
# The fee (as percentage) taken by the Validator from Delegators
fee_quotient: uint64
```
## Effective Balance, Validator Balance, Delegated Balances and Quotas

### Balances
For a Validator that is delegated, the `effective_balance` now contains the sum of delegated balances for that Validator.
### Quotas
To manage splitting of rewards/penalties/slashings a quota system was introduced in the DelegatedValidator.
Assuming that `1` (as in 100%) is the total quota of that DelegatedValidator, it gets split between the Validator's own balance: `state.balances[index]` and the delegations it received.
That means that every time the delegator's balance changes or a new delegation/undelegation/redelegation occurs, the quotas change and they must be recalculated.
```python
delegated_validator.delegated_validator_quota = (state.balances[validator_index] / (delegated_validator.total_delegated_balance + state.balances[validator_index]))
for index in range(len(delegated_validator.delegators_quotas)):
delegated_validator.delegators_quotas[index] = delegated_validator.delegated_balances[index] / delegated_validator.total_delegated_balance * (1 - delegated_validator.delegated_validator_quota)
```
We use the `delegated_balances` list to recompute the quotas, when needed.
## Delegation Actions
This chapter aims to explain how each action is funneled and processed by the system. It is required to understand how requests are accumulated in their respective buffers in the state (if not already, please read above).
### Activate Operator
`type: ACTIVATE_OPERATOR_REQUEST_TYPE`
`parsed to: PendingActivateOperator`
`queued to: state.pending_operator_activations`
`buffered on: block`
`drained on: block`
```python
class PendingActivateOperator(Container):
validator_pubkey: BLSPubkey
fee_quotient: uint64
source_address: ExecutionAddress
```
This action is used to enable delegations for a given validator. Requests are drained from the state by:
```python
def process_pending_activate_operators(state: BeaconState) -> None:
```
This method checks:
- if there exists a validator with given pubkey (`validator_pubkey`)
- if the validator is not already an operator
- if the validator has the correct credentials (compound)
- if the given `source_address` matches the one from the validator's withdraw credentials
- if the validator meets the `MIN_OPERATOR_BALANCE`
- if the validator is not exiting or slashed
Having all these conditions met, delegations can be enabled for this validator:
```python
class Validator(Container):
pubkey: BLSPubkey
withdrawal_credentials: Bytes32
effective_balance: Gwei
slashed: boolean
activation_eligibility_epoch: Epoch
activation_epoch: Epoch
exit_epoch: Epoch
withdrawable_epoch: Epoch
is_operator: boolean <== set this to True
```
Also a new `DelegatedValidator` is created and appended to the state, since this is the container that wraps around a Validator that accepts delegations:
```python
validator.is_operator = True
delegated_validator = DelegatedValidator(
validator=validator,
delegated_validator_quota=1,
delegators_quotas=[],
delegated_balances=[],
total_delegated_balance=0,
fee_quotient=pending_activation.fee_quotient
)
state.delegated_validators.append(delegated_validator)
```
---
### Deposit to Delegate
`type: DEPOSIT_TO_DELEGATE_REQUEST_TYPE`
`parsed to: PendingDepositToDelegate`
`queued to: state.pending_deposits_to_delegate`
`buffered on: block`
`drained on: epoch`
```python
class PendingDepositToDelegate(Container):
execution_address: ExecutionAddress
amount: Gwei
```
**This action is used to credit the non delegated balance of the delegator. The amount of ETH that was burned in the Deposit to Delegate Contract. A Delegator will be created and its balance set to the amount, OR if a Delegator already exists a top up will be performed.**
The method that drains the buffer is:
```python
def process_pending_deposits_to_delegate(state: BeaconState) -> None:
```
This method loops all items in the buffer, and creates a new `Delegator` and appends it to the state, OR performs a top-up for the given amount.
---
### Delegate
`type: DELEGATE_REQUEST_TYPE`
`parsed to: PendingDelegateRequest`
`queued to: state.pending_delegations`
`buffered on: block`
`drained on: epoch`
```python
class PendingDelegateRequest(Container):
execution_address: ExecutionAddress
validator_pubkey: BLSPubkey
amount: Gwei
slot: Slot
```
**Delegate means moving balance from the non delegated balance of a Delegator into the effective balance of a Validator that accepts delegations.**
The method that drains the requests and processes them is:
```python
def process_pending_delegations(state: BeaconState) -> None:
```
For each item in the buffer, this method makes some checks:
- checks if there is a Delegator and Validator for the given data
- checks if the validator is delegable
- checks how much we can process, to stay below churn
```python
available_for_processing = get_activation_exit_churn_limit(state)
processed_amount = 0
delegations_to_postpone = []
# processing starts here...
```
This follows the deposit logic and churn. We compute how much we can delegate, and what does not fit will be postponed for later (added back in the buffer).
There is a difference between the normal `deposit` and this, because here we can process the **entire amount** from the request and we can also process **a fraction of the amount** from the request, if that fraction fits in the remainder of the churn.
> Once a fraction or the entire amount has been processed and delegated, the request is discarded from the buffer.
---
### Undelegate
`type: UNDELEGATE_REQUEST_TYPE`
`parsed to: PendingUndelegateRequest`
`queued to: state.pending_undelegations`
`buffered on: block`
`drained on: epoch`
```python
class PendingUndelegateRequest(Container):
execution_address: ExecutionAddress
validator_pubkey: BLSPubkey
amount: Gwei
```
The method that drains the requests and processes them is:
```python
def process_pending_undelegations(state: BeaconState) -> None:
```
**Undelegate means moving balance from the effective balance of a Validator that accepts delegations into the the non delegated balance of a Delegator.**
Undelegations are affecting the financial security of the protocol, because the given amount must leave the staking pool. For this reason we need a way to control the amount that can exit.
For that reason we introduce the [ExitQueue](#The-Delegation-Exit-Queue).
The `process_pending_undelegations` method drains the `pending_undelegations` buffer and for each item:
- checks if the execution address belongs to the Delegator and if there is anything delegated under that identifier
- computes the `exit_epoch` and `withdrawable_epoch` for this request
- creates and queues a new `DelegationExitItem` into the delegation exit queue
All the items in `state.pending_undelegations` are processed in the same loop. The churning and limiting happens when the exit and withdrawability epochs are calculated:
```python
# Calculates the undelegation's exit and withdrawability epochs
exit_epoch = compute_exit_epoch_and_update_churn(state, requested_to_undelegate)
withdrawable_epoch = Epoch(exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY)
```
The `process_pending_undelegations` method stops here. Further processing of the DelegationExitItems is done in the [ExitQueue](#The-Delegation-Exit-Queue) pipeline.
---
### Redelegate
`type: REDELEGATE_REQUEST_TYPE`
`parsed to: PendingRedelegateRequest`
`queued to: state.pending_redelegations`
`buffered on: block`
`drained on: epoch`
```python
class PendingUndelegateRequest(Container):
execution_address: ExecutionAddress
validator_pubkey: BLSPubkey
amount: Gwei
```
The method that drains the requests and processes them is:
```python
def process_pending_undelegations(state: BeaconState) -> None:
```
**Redelegate is reallocating balance from the effective balance of a Validator that accepts delegations into the effective balance of another Validator that accepts delegations.**
Redelegation is an Undelegate + Delegate. The only difference is that the requested amount to be redelegated does not go through the Delegator's balance, it exits from `Source Validator` and delegates to `Target Validator`.
The draining method, for each item in the buffer:
- checks the source address and `Source Validator`
- checks if there is any balance delegated in `Source Validator`
- computes the `exit_epoch` and `withdrawable_epoch` for this request
- creates and queues a new `DelegationExitItem` into the delegation exit queue
The main difference here is the fact that the `DelegationExitItem` has some extra fields that are used for redelegation, and they are filled here.
```python
class DelegationExitItem(Container):
amount: Gwei
total_amount_at_exit: Gwei
execution_address: ExecutionAddress
validator_pubkey: BLSPubkey
exit_epoch: Epoch
withdrawable_epoch: Epoch
is_redelegation: boolean # <== redelegation
redelegate_to_validator_pubkey: BLSPubkey # <== redelegation
```
The `process_pending_undelegations` method stops here. Further processing of the DelegationExitItems is done in the [ExitQueue](#The-Delegation-Exit-Queue) pipeline.
---
### Withdraw from delegator
`type: WITHDRAW_FROM_DELEGATOR_REQUEST_TYPE`
`parsed to: PendingWithdrawFromDelegatorRequest`
`queued to: state.pending_withdrawals_from_delegators`
`buffered on: block`
`drained on: block`
```python
class PendingWithdrawFromDelegatorRequest(Container):
execution_address: ExecutionAddress
amount: Gwei
```
**Withdrawing from delegator is the action of removing balance from the non delegated balance of a Delegator into a wallet, on EL. In a way it's the inverse of Deposit To Delegate.**
This was implemented to mimic [EIP-7002: Execution layer triggerable withdrawals](https://eips.ethereum.org/EIPS/eip-7002) functionality. For that reason we need to drain the buffer on block.
A new method was created for that reason:
```python
def get_expected_withdrawals_from_delegators(state: BeaconState, execution_address: ExecutionAddress) -> None:
```
This method will pick item from the buffer based on:
- if they fit in the allocated slots
- `MAX_PENDING_WITHDRAWALS_FROM_DELEGATOR_PER_PAYLOAD=16` withdrawals per block, to keep the payload overhead small enough so that extra gas fees can be avoided
- the delegator is withdrawable
- `MIN_DELEGATOR_WITHDRAWABILITY_DELAY=2048` epochs was introduced. It forces a Delegator to keep his balance in the CL for a given amount of epochs, post creation, to avoid a needless ping-pong with ETH between EL and CL
- the delegator has enough balance
`prepare_execution_payload` gathers those items and sends them to the EL via:
```python
payload_attributes = PayloadAttributes(
timestamp=compute_time_at_slot(state, state.slot),
prev_randao=get_randao_mix(state, get_current_epoch(state)),
suggested_fee_recipient=suggested_fee_recipient,
withdrawals=withdrawals,
withdrawals_from_delegators=withdrawals_from_delegators, # <== here
parent_beacon_block_root=hash_tree_root(state.latest_block_header),
)
```
Withdrawals are then processed by `process_block` in `process_withdrawals_from_delegators`. It checks if `prepare_execution_payload` returns the same set of withdrawals that it got in the `ExecutionPayload` and if so, it dequeues the requests and decreaseses the non delegated balance of the Delegator.
## The Delegation Exit Queue
The delegation exit queue is a list that lives in the state:
```python
class BeaconState(Container):
# [...]
# Delegation exit queue
delegation_exit_queue: List[DelegationExitItem, PENDING_DELEGATION_OPERATIONS_LIMIT]
```
When a delegated amount must become undelegated, it has to wait in a buffer somewhere. That amount is still accountable for the actions of the Validator for N epochs --that represent the churn + M epochs -- that handle Weak Subjectivity. We use the Delegation Exit Queue for this purpose.
This list is used by 2 actions:
- Undelegate
- Redelegate (because of the Undelegate part of Redelegate)
Inside the list we find `DelegationExitItem`:
```python
class DelegationExitItem(Container):
amount: Gwei
total_amount_at_exit: Gwei
execution_address: ExecutionAddress
validator_pubkey: BLSPubkey
exit_epoch: Epoch
withdrawable_epoch: Epoch
is_redelegation: boolean
redelegate_to_validator_pubkey: BLSPubkey
```
- **amount** defines the amount that should be undelegated
- **total_amount_at_exit** this is set when the `exit_epoch` arrives. That is the point in which the delegation is *subtracted* from the Validator's delegated balance. We store here how much was delegated + the balance of the Validator at that point, to be able to determine later (for rewards, penalties and slashings) what was the quota of this delegated amount
- **exit_epoch** is the epoch in which the `amount` will be subtracted from the `delegated_balances` of the Delegated Validator
- **withdrawable_epoch** is the epoch in which the `amount` will be credited to the non delegated balance of the Delegator
- **is_redelegation** defines if the `DelegationExitItem` will be redelegated after the `withdrawable_epoch` arrives
- **redelegate_to_validator_pubkey** defines the target Validator for a future Delegation (the second part of Redelegate)
---
**The exit queue is used by multiple pipelines:**
1. Undelegate
2. Redelegate
3. Slashings
4. Validator exit
#### Undelegate and Redelegate
Each Undelegate or Redelegate request from the buffer is eventually added to the delegation exit queue.
On each epoch boundary `process_delegation_exit_queue` runs.
It checks each `DelegationExitItem` in the queue and:
- `if current_epoch >= delegation_exit.exit_epoch` it executes the *exit*. The `amount` is withdrawn from the `delegated_balances`, and the `total_amount_at_exit` is set. The item remains in the queue, awaiting the next milestone: the withdrawable_epoch.
- `if current_epoch >= delegation_exit.withdrawable_epoch` it's time to *settle* the undelegation. This means that the `amount` is credited to the non delegated balance of the Delegator. The `DelegationExitItem` is removed from the queue since it's done its part.
- `if delegation_exit.is_redelegation` we don't actually credit the Delegator, we queue a new `PendingDelegateRequest` into `state.pending_delegations`. Basically we create a new Delegation Request, which will be processed by the Delegation pipeline from here on. The `DelegationExitItem` is removed from the queue since it's done its part.
#### Validator exit
When a validator that has delegations enabled exits it must undelegate those delegations.
`initiate_validator_exit` has been modified to account for this scenario:
```python
def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None:
#[...]
if validator.is_operator:
initiate_delegated_balances_exit(state, validator.pubkey, validator.exit_epoch, validator.withdrawable_epoch)
```
The method `initiate_delegated_balances_exit` loops through every delegated amount and queues a new `DelegationExitItem` in the Delegation Exit Queue.
## Slashings, rewards and penalties
The delegated amounts must be slashable. And they must accrue rewards and penalties.
The balance that is not delegated (the non delegated balance of the Delegator) does not participate in the protocol and is not held accountable. It does not get slashed and it does not produce rewards. It is idle, ready to be delegated or withdrawn to a wallet.
An amount **is slashable** while delegated, but also inside the delegation exit queue, until `withdrawable_epoch` is reached.
An amount **generates rewards and penalties** while delegated, but NOT inside the delegation exit queue, untill `exit_epoch` is reached.
### Slashing

Slashing must now also take into consideration the qualifying DelegationExitItems from the delegation exit queue. A delegated amount can NOT leave a Validator instantly, and it MUST remain accountable for an extra period of time (WS) even in the delegation exit queue.
To be more specific, it is slashable **in the delegation exit queue** if:
`exit_epoch <= current_epoch <= withdrawability_epoch`
If `current_epoch < exit_epoch`, it is slashable in the DelegatedValidator, because that amount is still participating in the protocol, and will be slashed only there.
The way slashing is handled in eODS is as follows:
#### 1. We slash the Delegation Exit Queue

Assuming Validator with pubkey `abcd` must be slashed, we pick from the delegation exit queue the items that qualify. We look at the Validator pubkey and epochs.
To slash an DelegationExitItem we calculate the delegated_quota of that item. This is important to know because for any slashing we only know the total amount of slashing we must apply, and we need to divide that between every DelegationExitItem that qualifies, the participating delegated balances, and the Validator's own balance.
So:
```python
total_slashed_in_queue = 0
# we filter the delegation exit queue into list_to_slash
for index in range(len(list_to_slash )):
exit_item = state.list_to_slash[index]
# figure out how much of that total slash we apply to THIS exit item
delegated_quota = exit_item.amount / exit_item.total_amount_at_exit
to_slash = delegated_quota * penalty
# we accumulate all slashings applied so far to the delegation exit queue items
total_slashed_in_queue += to_slash
state.delegation_exit_queue[index].undelegated_amount -= to_slash
# return this value, because be need to see what's left for the active delegated balances and the Validator's balance
return total_slashed_in_queue
```
#### 2. We slash the active delegated balances and the Validator's own balance
```python
# slash the delegation exit queue and grab the total slashed there
slashed_in_queue = slash_exit_queue(state, validator_pubkey, penalty)
# this we split between active delegated balances and Validator's own balance
rest_to_slash = penalty - slashed_in_queue
delegated_validator = get_delegated_validator(state, validator_pubkey)
# calculate the validator's slashing
validator_penalty = delegated_validator.delegated_validator_quota * rest_to_slash
# calculate the amount of slashing to be applied for the active delegated balances
delegators_penalty = rest_to_slash - validator_penalty
# slash the operator
decrease_balance(state, ValidatorIndex(validator_index), validator_penalty)
# slash the delegations
apply_delegations_slashing(delegated_validator, delegators_penalty)
```
```python
def apply_delegations_slashing(delegated_validator: DelegatedValidator, penalty: Gwei, ) -> None:
delegated_validator.total_delegated_balance -= penalty
# slash the delegated balances based on the quota they own from the total effective_balance
for index in range(len(delegated_validator.delegated_balances)):
delegated_validator.delegated_balances[index] -= penalty * delegated_validator.delegators_quotas[index]
```
So by using a quota system we can figure out exactly how much slashing we must apply for all accountable parts.
All this functionality is encapsulated in `slash_delegated_validator_and_exit_queue`, part of the Beacon Chain Accounting.
```python
def slash_delegated_validator_and_exit_queue(state: BeaconState, validator_index: ValidatorIndex, validator_pubkey: BLSPubkey, penalty: Gwei) -> None:
```
### Rewards and penalties
Delegated balances accumulate rewards and penalties. This is true from the moment that delegation becomes active, up to its exit epoch.
Unlike slashings, the delegation exit queue elements are not subject to receiving rewards and penalties because rewards and penalties do not occur unless the balance is active. The rewards and penalties are applied only in the DelegatedValidator, on top of the `delegated_balances`.

The rewards are split between the Validator and the participating delegated balances, based on their contribution quotas.
```python
def reward_delegated_validator(state: BeaconState, validator_index: ValidatorIndex,
validator_pubkey: BLSPubkey, reward: Gwei) -> None:
delegated_validator = get_delegated_validator(state, validator_pubkey)
validator_reward = delegated_validator.delegated_validator_quota * reward
delegators_reward = reward - validator_reward
# reward the operator
increase_balance(state, ValidatorIndex(validator_index), validator_reward)
# reward the delegations
apply_delegations_rewards(delegated_validator, delegators_reward)
```
```python
def apply_delegations_rewards(amount: Gwei, delegated_validator: DelegatedValidator) -> None:
delegated_validator.total_delegated_balance += amount
for index in range(len(delegated_validator.delegators_quotas)):
delegated_validator.delegated_balances[index] += amount * delegated_validator.delegators_quotas[index]
```
Applying penalties follow the same principle.
## Closing thoughts
These spec changes aim to be a starting point for describing a minimal delegation integration within the exiting feature set.
eODS in itself has a medium to large footprint in the exiting model, but the intersection points with said model are kept to a minimum.
There are a few notable changes brought:
- The EL-CL data transfer bus, further using [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685)
- The introduction of a [delegation exit queue](#The-Delegation-Exit-Queue) for the ability to keep delegated amounts accountable throughout the exit process, WS included
- The addition of the [Delegated Validator](#Added-DelegatedValidator) as a wrapper on top of the existing Validator, keeping Validator changes to a minimum
- The [Delegations Operations Contract](#Delegation-Operations-Contract), and the idea that we can massively improve UX and adoption just by using the tools we already have