--- eip: 8148 title: Custom sweep threshold for validators description: Allow setting custom balance thresholds for sweep withdrawals for validators with compounding withdrawal credentials (`0x02`). author: Dmitry Gusakov (@dgusakov), Dmitry Chernukhin (@madlabman), Greg Koumoutsos (@gkoumout), Manu (@nalepae) discussions-to: https://ethereum-magicians.org/t/eip-8148-custom-sweep-threshold-for-validators/27669 status: Draft type: Standards Track category: Core created: 2026-02-05 requires: 7251, 7685 --- ## Abstract This EIP proposes an optional mechanism to set custom balance thresholds for sweep withdrawals for compounding withdrawal credentials (`0x02`) validators. This mechanism allows validators to specify above which balance they want their rewards to be swept to their withdrawal address, providing greater flexibility and control over their staking rewards. ## Motivation The current default sweep threshold (2,048 ETH) for validators using compounding withdrawal credentials (`0x02`) may not meet the needs of all validators. Some validators may prefer to accumulate rewards on the validator balance, while others may want to sweep before reaching the current threshold of 2,048 ETH. By allowing optional custom sweep thresholds, validators can optimize their reward management according to their individual strategies and preferences. Since the introduction of the `0x02` compounding withdrawal credentials type, we have observed a very low rate of validators transitioning to `0x02`. One reason is that many validators do not want to wait until they accumulate 2048 ETH in rewards before being able to participate in the automatic sweep of withdrawals. While partial withdrawals were considered a viable method for manually withdrawing portions of the validator balance, this approach was not widely adopted by staking protocols, node operators, and solo stakers for several reasons. First, it requires a user-initiated transaction to perform a withdrawal. Second, partial withdrawals utilize the general exit queue, which makes the time between partial withdrawal initiation and fulfillment unpredictable and heavily dependent on network conditions (see the recent spike in exit queue size in October 2025). This EIP aims to address this issue by allowing validators to set a custom threshold for sweep withdrawals. A simple example illustrates the utility of this feature. Consider a validator who wishes to accumulate rewards on their validator balance until reaching 128 ETH, at which point they want to sweep the rewards to their withdrawal address. Without this feature, the validator would have to initiate partial withdrawals at certain intervals manually and wait in the partial withdrawals queue, which can be time-consuming and inconvenient. The first inconvenience is that if staking protocols widely adopt partial withdrawals at some point, the queue for these withdrawals might become long and unpredictable, similar to the exit queue. The second inconvenience is that the user must monitor the validator's balance and manually initiate partial withdrawals, which adds complexity and overhead to the staking process. The third inconvenience is the frequency with which the validator can request partial withdrawals. A 128-ETH validator will receive approximately 0.07 ETH in rewards each week. Initiating partial withdrawals TX, for such a low amount might be considered unreasonable. At the same time, the sweep cycle will likely drop to a weekly cycle relatively soon, allowing the validator to automatically receive these 0.07 ETH of rewards on their withdrawal credentials. In general, with this EIP, the validator can set their desired sweep threshold and automatically benefit from sweep withdrawals. The proposed mechanism is completely optional and does not change anything in the default registration / withdrawal / exit process for validators. It's just an additional feature that could be ignored if not interesting, but provides a useful feature for many validators incentivizing switching to compounding validators. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). ### Constants #### Execution layer | Name | Value | Comment | | ----------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------ | | `SWEEP_THRESHOLD_REQUEST_TYPE` | `0x05` | The [EIP-7685](./eip-7685.md) type prefix for set sweep threshold request | | `SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS` | `TBD` | Where to call and store relevant details about set sweep threshold request mechanism | | `SYSTEM_ADDRESS` | `0xfffffffffffffffffffffffffffffffffffffffe` | Address used to invoke system operation on contract | | `EXCESS_SET_SWEEP_THRESHOLD_REQUESTS_STORAGE_SLOT` | `0` | | | `SET_SWEEP_THRESHOLD_REQUEST_COUNT_STORAGE_SLOT` | `1` | | | `SET_SWEEP_THRESHOLD_REQUEST_QUEUE_HEAD_STORAGE_SLOT` | `2` | Pointer to the head of the set sweep threshold request message queue | | `SET_SWEEP_THRESHOLD_REQUEST_QUEUE_TAIL_STORAGE_SLOT` | `3` | Pointer to the tail of the set sweep threshold request message queue | | `SET_SWEEP_THRESHOLD_REQUEST_QUEUE_STORAGE_OFFSET` | `4` | The start storage slot of the in-state set sweep threshold request message queue | | `MAX_SET_SWEEP_THRESHOLD_REQUESTS_PER_BLOCK` | `16` | Maximum number of set sweep threshold requests that can be dequeued into a block | | `TARGET_SET_SWEEP_THRESHOLD_REQUESTS_PER_BLOCK` | `2` | | | `MIN_SET_SWEEP_THRESHOLD_REQUEST_FEE` | `1` | | | `SET_SWEEP_THRESHOLD_REQUEST_FEE_UPDATE_FRACTION` | `17` | | | `EXCESS_INHIBITOR` | `2**256-1` | Excess value used to compute the fee before the first system call | #### Consensus layer | Name | Value | Comment | | ---------------------------------------------- | ----- | -------------------------------------------------------------------------- | | `SWEEP_THRESHOLD_CREDENTIAL_OFFSET` | `1` | Byte offset of the threshold embedded in `0x02` withdrawal credentials | | `SWEEP_THRESHOLD_CREDENTIAL_LENGTH` | `2` | Length in bytes of the threshold embedded in `0x02` withdrawal credentials | | `MAX_SET_SWEEP_THRESHOLD_REQUESTS_PER_PAYLOAD` | `16` | Maximum number of set sweep threshold requests in each payload | ### Execution layer #### Definitions - **`FORK_BLOCK`** -- the first block in a blockchain after this EIP has been activated. #### Set sweep threshold request The new set sweep threshold request is an [EIP-7685](./eip-7685.md) request with type `SWEEP_THRESHOLD_REQUEST_TYPE` consisting of the following fields: 1. `source_address`: `Bytes20` 2. `validator_pubkey`: `Bytes48` 3. `threshold`: `uint64`, in Gwei The [EIP-7685](./eip-7685.md) encoding of a set sweep threshold request is computed as follows. Note that `threshold` is returned by the contract little-endian, and must be encoded as such. This differs from the contract input and log, where `threshold` is big-endian. ```python request_type = SWEEP_THRESHOLD_REQUEST_TYPE request_data = read_set_sweep_threshold_requests() ``` #### Set sweep threshold request contract The contract has three different code paths, which can be summarized at a high level as follows: 1. Add set sweep threshold request - requires a 56-byte input: validator public key concatenated with a big-endian `uint64` threshold value in Gwei. 2. Fee getter - if the input length is zero, return the current fee required to add a set sweep threshold request. 3. System process - if called by the system address, pop off the set sweep threshold requests for the current block from the queue. ##### Add Set Sweep Threshold Request If call data input to the contract is exactly `56` bytes, it is interpreted as: | Bytes | Field | Description | | ------- | ------------------ | ---------------------------- | | `0:48` | `validator_pubkey` | 48-byte BLS public key | | `48:56` | `threshold` | Big-endian `uint64`, in Gwei | The contract then performs the following: 1. Ensure enough ETH was sent to cover the current set sweep threshold request fee (`msg.value >= get_fee()`). 2. Increase set sweep threshold request count by 1 for the current block. 3. Insert a set sweep threshold request into the queue for the source address, validator public key, and the threshold. 4. Emit the accepted request as an anonymous log (`LOG0`) with `76` bytes of data: `source_address (20) ++ validator_pubkey (48) ++ threshold (8)`, where `source_address` is `msg.sender` and `threshold` is big-endian as in the input. Specifically, the functionality is defined in pseudocode as the function `add_set_sweep_threshold_request()`: ```python def add_set_sweep_threshold_request(validator_pubkey: Bytes48, threshold: uint64): """ Add a new request to the set sweep threshold request queue, provided a sufficient value to cover the fee was sent. """ # Verify sufficient value was provided. fee = get_fee() require(msg.value >= fee, 'Insufficient value for fee') # Increment the request count. count = sload(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, SET_SWEEP_THRESHOLD_REQUEST_COUNT_STORAGE_SLOT) sstore(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, SET_SWEEP_THRESHOLD_REQUEST_COUNT_STORAGE_SLOT, count + 1) # Insert into the queue. queue_tail_index = sload(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, SET_SWEEP_THRESHOLD_REQUEST_QUEUE_TAIL_STORAGE_SLOT) queue_storage_slot = SET_SWEEP_THRESHOLD_REQUEST_QUEUE_STORAGE_OFFSET + queue_tail_index * 3 sstore(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot, msg.sender) sstore(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 1, validator_pubkey[ 0:32]) sstore(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 2, validator_pubkey[32:48] ++ threshold) sstore(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, SET_SWEEP_THRESHOLD_REQUEST_QUEUE_TAIL_STORAGE_SLOT, queue_tail_index + 1) # Emit the accepted request. log0(msg.sender ++ validator_pubkey ++ threshold) ``` ###### Fee calculation The following pseudocode can compute the cost of an individual set sweep threshold request, given a certain number of excess set sweep threshold requests. ```python def get_fee() -> int: excess = sload(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, EXCESS_SET_SWEEP_THRESHOLD_REQUESTS_STORAGE_SLOT) require(excess != EXCESS_INHIBITOR, 'Inhibitor still active') return fake_exponential( MIN_SET_SWEEP_THRESHOLD_REQUEST_FEE, excess, SET_SWEEP_THRESHOLD_REQUEST_FEE_UPDATE_FRACTION ) def fake_exponential(factor: int, numerator: int, denominator: int) -> int: i = 1 output = 0 numerator_accum = factor * denominator while numerator_accum > 0: output += numerator_accum numerator_accum = (numerator_accum * numerator) // (denominator * i) i += 1 return output // denominator ``` ##### Fee Getter When the input to the contract has zero-length, interpret this as a get request for the current fee, i.e. the contract returns the result of `get_fee()`. The contract reverts if any value is sent to prevent loss of funds. ##### System Call At the end of processing any execution block starting from the `FORK_BLOCK` (i.e. after processing all transactions), call `SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS` as `SYSTEM_ADDRESS` with no calldata. The invocation triggers the following: - The contract's queue is updated based on set sweep threshold requests dequeued and the set sweep threshold requests queue head/tail are reset if the queue has been cleared (`dequeue_set_sweep_threshold_requests()`) - The contract's excess set sweep threshold requests are updated based on usage in the current block (`update_excess_set_sweep_threshold_requests()`) - The contract's set sweep threshold requests count is reset to 0 (`reset_set_sweep_threshold_requests_count()`) In response to the system call, the contract returns an opaque byte array of concatenated SSZ-serialized dequeued requests. There's no specific reasoning behind it, except aligning with the existing behaviour of the similar EIP, see [EIP-7002](./eip-7002.md), and possible simplification of the processing flow for client teams. Each set sweep threshold request must appear in the EIP-7685 requests list in the exact order returned by `dequeue_set_sweep_threshold_requests()`. Additionally, the system call and the processing of that block must conform to the following: - The call has a dedicated gas limit of `30_000_000` (`SYSTEM_TRANSACTION_GAS`) and is not subject to the transaction limit cap introduced in [EIP-7825](./eip-7825.md). - Gas consumed by this call does not count against the block's overall gas usage. - Both the gas limit assigned to the call and the gas consumed are excluded from any checks against the block's gas limit. - The call does not follow [EIP-1559](./eip-1559.md) fee burn semantics — no value should be transferred as part of this call. - If there is no code at `SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS`, the corresponding block **MUST** be marked invalid. - If the call to the contract fails or returns an error, the block **MUST** be invalidated. The functionality triggered by the system call is defined in pseudocode as the function `read_set_sweep_threshold_requests()`: ```python ################### # Public function # ################### def read_set_sweep_threshold_requests(): reqs = dequeue_set_sweep_threshold_requests() update_excess_set_sweep_threshold_requests() reset_set_sweep_threshold_requests_count() return b"".join(ssz.serialize(r) for r in reqs) ########### # Helpers # ########### class ValidatorSetSweepThresholdRequest(Container): source_address: Bytes20 validator_pubkey: Bytes48 threshold: uint64 def dequeue_set_sweep_threshold_requests(): queue_head_index = sload(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, SET_SWEEP_THRESHOLD_REQUEST_QUEUE_HEAD_STORAGE_SLOT) queue_tail_index = sload(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, SET_SWEEP_THRESHOLD_REQUEST_QUEUE_TAIL_STORAGE_SLOT) num_in_queue = queue_tail_index - queue_head_index num_dequeued = min(num_in_queue, MAX_SET_SWEEP_THRESHOLD_REQUESTS_PER_BLOCK) reqs = [] for i in range(num_dequeued): queue_storage_slot = SET_SWEEP_THRESHOLD_REQUEST_QUEUE_STORAGE_OFFSET + (queue_head_index + i) * 3 source_address = address(sload(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot)[0:20]) validator_pubkey = ( sload(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 1)[0:32] + sload(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 2)[0:16] ) threshold = sload(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, queue_storage_slot + 2)[16:24] req = ValidatorSetSweepThresholdRequest( source_address=Bytes20(source_address), validator_pubkey=Bytes48(validator_pubkey), threshold=uint64(threshold) ) reqs.append(req) new_queue_head_index = queue_head_index + num_dequeued if new_queue_head_index == queue_tail_index: # Queue is empty, reset queue pointers sstore(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, SET_SWEEP_THRESHOLD_REQUEST_QUEUE_HEAD_STORAGE_SLOT, 0) sstore(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, SET_SWEEP_THRESHOLD_REQUEST_QUEUE_TAIL_STORAGE_SLOT, 0) else: sstore(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, SET_SWEEP_THRESHOLD_REQUEST_QUEUE_HEAD_STORAGE_SLOT, new_queue_head_index) return reqs def update_excess_set_sweep_threshold_requests(): # A system call with non-empty calldata disables the queue if len(msg.calldata) > 0: sstore(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, EXCESS_SET_SWEEP_THRESHOLD_REQUESTS_STORAGE_SLOT, EXCESS_INHIBITOR) return previous_excess = sload(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, EXCESS_SET_SWEEP_THRESHOLD_REQUESTS_STORAGE_SLOT) if previous_excess == EXCESS_INHIBITOR: previous_excess = 0 count = sload(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, SET_SWEEP_THRESHOLD_REQUEST_COUNT_STORAGE_SLOT) new_excess = 0 if previous_excess + count > TARGET_SET_SWEEP_THRESHOLD_REQUESTS_PER_BLOCK: new_excess = previous_excess + count - TARGET_SET_SWEEP_THRESHOLD_REQUESTS_PER_BLOCK sstore(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, EXCESS_SET_SWEEP_THRESHOLD_REQUESTS_STORAGE_SLOT, new_excess) def reset_set_sweep_threshold_requests_count(): sstore(SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS, SET_SWEEP_THRESHOLD_REQUEST_COUNT_STORAGE_SLOT, 0) ``` ##### Bytecode The following bytecode is produced by the geas compiler from the source code in the sys-asm repository. ```asm caller push20 0xfffffffffffffffffffffffffffffffffffffffe eq push1 0xcb jumpi push1 0x11 push0 sload dup1 push32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff eq push2 0x021c jumpi push1 0x01 dup3 mul push1 0x01 swap1 push0 jumpdest push0 dup3 gt iszero push1 0x68 jumpi dup2 add swap1 dup4 mul dup5 dup4 mul swap1 div swap2 push1 0x01 add swap2 swap1 push1 0x4d jump jumpdest swap1 swap4 swap1 div swap3 pop pop pop calldatasize push1 0x38 eq push1 0x88 jumpi calldatasize push2 0x021c jumpi callvalue push2 0x021c jumpi push0 mstore push1 0x20 push0 return jumpdest callvalue lt push2 0x021c jumpi push1 0x01 sload push1 0x01 add push1 0x01 sstore push1 0x03 sload dup1 push1 0x03 mul push1 0x04 add caller dup2 sstore push1 0x01 add push0 calldataload dup2 sstore push1 0x01 add push1 0x20 calldataload swap1 sstore caller push1 0x60 shl push0 mstore push1 0x38 push0 push1 0x14 calldatacopy push1 0x4c push0 log0 push1 0x01 add push1 0x03 sstore stop jumpdest push1 0x03 sload push1 0x02 sload dup1 dup3 sub dup1 push1 0x10 gt push1 0xdf jumpi pop push1 0x10 jumpdest push0 jumpdest dup2 dup2 eq push2 0x0183 jumpi dup3 dup2 add push1 0x03 mul push1 0x04 add dup2 push1 0x4c mul dup2 sload push1 0x60 shl dup2 mstore push1 0x14 add dup2 push1 0x01 add sload dup2 mstore push1 0x20 add swap1 push1 0x02 add sload dup1 push32 0xffffffffffffffffffffffffffffffff00000000000000000000000000000000 and dup3 mstore swap1 push1 0x10 add swap1 push1 0x40 shr swap1 dup2 push1 0x38 shr dup2 push1 0x07 add mstore8 dup2 push1 0x30 shr dup2 push1 0x06 add mstore8 dup2 push1 0x28 shr dup2 push1 0x05 add mstore8 dup2 push1 0x20 shr dup2 push1 0x04 add mstore8 dup2 push1 0x18 shr dup2 push1 0x03 add mstore8 dup2 push1 0x10 shr dup2 push1 0x02 add mstore8 dup2 push1 0x08 shr dup2 push1 0x01 add mstore8 mstore8 push1 0x01 add push1 0xe1 jump jumpdest swap2 add dup1 swap3 eq push2 0x0195 jumpi swap1 push1 0x02 sstore push2 0x01a0 jump jumpdest swap1 pop push0 push1 0x02 sstore push0 push1 0x03 sstore jumpdest calldatasize push2 0x01ee jumpi push0 sload push1 0x01 sload dup2 push32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff eq push2 0x01dc jumpi push1 0x02 dup3 dup3 add gt push2 0x01e4 jumpi jumpdest pop pop push0 push2 0x0210 jump jumpdest add push1 0x02 swap1 sub push2 0x0210 jump jumpdest push32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff jumpdest push0 sstore push0 push1 0x01 sstore push1 0x4c mul push0 return jumpdest push0 push0 revert ``` ##### Deployment The set sweep threshold requests contract is deployed like any other smart contract. A special synthetic address is generated by working backwards from the desired deployment transaction: ```json TBD ``` ``` Sender: TBD Address: TBD ``` ##### Chain configuration Clients exposing [EIP-7910](./eip-7910.md) `eth_config` MUST list the contract in `systemContracts` under the key `SET_SWEEP_THRESHOLD_REQUEST_PREDEPLOY_ADDRESS` for every fork in which this EIP is active. ### Consensus layer The defining feature of this EIP is ***allowing validators to set custom sweep thresholds for their withdrawals when using compounding withdrawal credentials (`0x02`)***. The [Rationale](#rationale) section contains an explanation for this proposed core feature. A validator's sweep threshold is stored in its compounding withdrawal credentials, so no field is added to `BeaconState` or `Validator`: | Bytes | Contents | | -------- | --------------------------------------------------------------------------------------------- | | `0` | `COMPOUNDING_WITHDRAWAL_PREFIX` (`0x02`) | | `1..2` | Threshold, little-endian `uint16` in `EFFECTIVE_BALANCE_INCREMENT` units, `0` for the default | | `3..11` | Reserved | | `12..31` | Execution address | A threshold of `MAX_EFFECTIVE_BALANCE_ELECTRA` is stored as `0`, and an out-of-range value is read as the default. Everywhere the sweep and effective balance logic use `get_max_effective_balance`, they use the threshold instead. Additionally: - A new `0x02` validator takes its threshold from the credentials of the deposit that creates it, with out-of-range values cleared, and its initial effective balance is capped at that threshold. - A set sweep threshold request rewrites bytes `1..2`. It is accepted only from the validator's execution address, for a non-exiting compounding validator, and for a threshold that is a multiple of `EFFECTIVE_BALANCE_INCREMENT`, between `MIN_ACTIVATION_BALANCE` and `MAX_EFFECTIVE_BALANCE_ELECTRA`, and not below the validator's current balance. - `switch_to_compounding_validator` and the fork clear bytes `1..2`, so switched and existing validators start at the default. - `ExecutionRequests` gains a list of `SetSweepThresholdRequest` items, processed alongside the other execution requests. Full consensus layer specification can be found in `specs/_features/eip8148/beacon-chain.md`. ## Rationale ### Overview Most of the considerations regarding the messaging format, queue, and rate-limiting are similar to those discussed in [EIP-7002](./eip-7002.md) for withdrawal requests, and so we refer the reader to that EIP for more details. ### Custom Sweep Thresholds The primary motivation for this EIP is to allow validators to set custom sweep thresholds for their withdrawals when using compounding withdrawal credentials (`0x02`). This feature provides greater flexibility and control over how and when validators can access their staking rewards. A `0x02` validator with a threshold of `MIN_ACTIVATION_BALANCE` behaves like a `0x01` validator, so compounding credentials cover every behaviour of execution credentials. ### Storing the threshold in withdrawal credentials A per-validator list in `BeaconState` would add 8 bytes for every validator in the registry, comparable to `balances`, to hold an 11-bit value that almost every validator leaves at its default. Compounding withdrawal credentials already carry 11 unused bytes, and the protocol already rewrites credentials in `switch_to_compounding_validator`, so the threshold is stored there. Two bytes is the smallest byte-aligned field that holds every valid value, placed right after the prefix and encoded little-endian like every other consensus layer integer (`0x02e803...` is 1000 ETH). Storing the maximum as `0` gives every threshold one encoding and lets a validator restore its original credentials. ### Only allowing threshold to be set above current balance This design decision is made to prevent usage of the custom sweep threshold mechanism to trigger immediate withdrawals. By enforcing that the threshold must be set above the current balance, we ensure that validators cannot use this feature to bypass the standard withdrawal process. Should a validator wish to set sweep threshold below current balance, they can first withdraw down to the desired level using partial withdrawals, and then set the sweep threshold accordingly. ### Immediate requests processing instead of queuing on consensus layer Unlike partial withdrawal requests, which are queued on the consensus layer, set sweep threshold requests are processed immediately upon being dequeued from the execution layer contract. This design choice simplifies the implementation and reduces the complexity of managing a separate queue on the consensus layer. ### Custom Sweep Threshold should be a multiple of `EFFECTIVE_BALANCE_INCREMENT` To maintain consistency with the existing balance increments in the protocol, we require that the custom sweep threshold be a multiple of `EFFECTIVE_BALANCE_INCREMENT` (1 ETH). This ensures that the sweep thresholds align with the existing effective balance structure and rounding, and it is what allows the threshold to be stored in two bytes of the withdrawal credentials. ## Backwards Compatibility This EIP introduces backwards incompatible changes to the block structure and block validation rule set. But neither of these changes break anything related to the current user activity and experience. ### Disabling the queue A system call with non-empty calldata stores `EXCESS_INHIBITOR` into the excess slot and thereby disables the queue; a later system call with empty calldata restores it, through the same code path that clears the inhibitor at activation. Under this EIP every system call is made with empty calldata, so the path is unreachable. It exists for a future upgrade that retires or suspends the contract: without it, once the protocol stops making the per-block system call, the contract would keep accepting requests and fees into a queue that never drains. ### Usage of the previously unused bytes in compounding withdrawal credentials Bytes `1` and `2` of compounding withdrawal credentials gain a meaning. The fork clears them for existing validators, and deposits that leave them zero, which is every deposit produced by tooling written before this EIP, receive the default threshold. Setting a custom threshold changes those bytes, so applications that compare all 32 bytes of withdrawal credentials, such as [EIP-4788](./eip-4788.md) proof verifiers, should compare the prefix and execution address instead. The change is opt-in: only the withdrawal address can submit a request, so a contract acting as a withdrawal address controls whether its validators' credentials ever change. ### Execution Layer Requests This EIP adds new type of the EL to CL request. This changes `ExecutionRequests` and requires coordinated approach to `execution_requests_root` validation at the fork. ## Security Considerations Most of the security considerations regarding fee overpayment, system call failure, and empty code failure are similar to those discussed in [EIP-7002](./eip-7002.md) for withdrawal requests, and so we refer the reader to that EIP for more details. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).