EIP-8148 - Custom sweep threshold for validators

Created 2026-02-05
Status Draft
Category Core
Type Standards Track
Authors
Requires

Abstract

This EIP proposes an optional mechanism to set custom balance thresholds for sweep withdrawals for compounding withdrawal credentials (0x02, 0x03) 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, 0x03) 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 and RFC 8174.

Constants

Execution layer

Name Value Comment
SET_SWEEP_THRESHOLD_REQUEST_TYPE 0x03 The EIP-7685 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
MIN_SWEEP_THRESHOLD MIN_ACTIVATION_BALANCE + Gwei(1 * 10**9) (33 ETH)

Execution layer

Definitions

Set sweep threshold request

The new set sweep threshold request is an EIP-7685 request with type SET_SWEEP_THRESHOLD_REQUEST_TYPE consisting of the following fields:

  1. source_address: Bytes20
  2. validator_pubkey: Bytes48
  3. threshold: uint64

The EIP-7685 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.

request_type = SET_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.
  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, perform 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.

Specifically, the functionality is defined in pseudocode as the function add_set_sweep_threshold_request():

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)
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.

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:

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, 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 functionality triggered by the system call is defined in pseudocode as the function read_set_sweep_threshold_requests():

###################
# 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():
    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.

caller
push20 0xfffffffffffffffffffffffffffffffffffffffe
eq
push1 0xcb
jumpi

push1 0x11
push0
sload
dup1
push32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
eq
push2 0x01f4
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 0x01f4
jumpi

callvalue
push2 0x01f4
jumpi

push0
mstore
push1 0x20
push0
return

jumpdest
callvalue
lt
push2 0x01f4
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 0x02
gt
push1 0xdf
jumpi

pop
push1 0x02

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
push0
sload
dup1
push32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
eq
iszero
push2 0x01cd
jumpi

pop
push0

jumpdest
push1 0x01
sload
push1 0x01
dup3
dup3
add
gt
push2 0x01e2
jumpi

pop
pop
push0
push2 0x01e8
jump

jumpdest
add
push1 0x01
swap1
sub

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:

TBD
Sender: TBD
Address: TBD

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, 0x03).

The Rationale section contains an explanation for this proposed core feature. A sketch of the resulting changes to the consensus layer is included below.

  1. Update the BeaconState container to include a validator_sweep_thresholds mapping.
  2. Add SetSweepThresholdRequest container to represent the set sweep threshold requests dequeued from the execution layer contract.
  3. Update the ExecutionRequests container to include a list of SetSweepThresholdRequest items.
  4. Add process_set_sweep_threshold_request function to handle the processing of set sweep threshold requests from the execution layer.
  5. Modify the process_execution_payload function to include the processing of set sweep threshold requests.
  6. Modify the is_partially_withdrawable_validator predicate to take into account the custom sweep threshold.
  7. Add get_effective_sweep_threshold helper function to compute the effective sweep threshold for a validator.
  8. Modify the get_expected_withdrawals function to use the custom sweep threshold when determining partial withdrawals.

By default, all validators will have their sweep thresholds set to the current default MAX_EFFECTIVE_BALANCE, both for existing validators and new ones. Validators can choose to set a custom threshold above their current balance by submitting a set sweep threshold request through the execution layer contract.

Rationale

Overview

Most of the considerations regarding the messaging format, queue, and rate-limiting are similar to those discussed in EIP-7002 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, 0x03). This feature provides greater flexibility and control over how and when validators can access their staking rewards.

validator_sweep_thresholds mapping in BeaconState

To store the custom sweep thresholds for each validator, we introduce a new mapping in the BeaconState container called validator_sweep_thresholds. This mapping associates each validator index with its corresponding sweep threshold. This approach was chosen instead of adding a new field to the Validator container to avoid modification of this type, which had not been changed since phase-0. Modification of the Validator container would have required more extensive changes to the consensus layer and potentially affected existing implementations of the applications using this container. Also, this is the standard way of adding info about validators into the state (e.g., validator balance is stored in balances field of BeaconState and several other PoS-related info have their own lists where each item corresponds to validators' data)

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.

MIN_SWEEP_THRESHOLD of 33 ETH

To ensure that validators do not set sweep threshold equal to MIN_ACTIVATION_BALANCE, we introduce a minimum sweep threshold of MIN_ACTIVATION_BALANCE + 1 ETH (33 ETH). This ensures that people will opt-in to compounding withdrawal credentials only if they really want to accumulate rewards on the validator balance.

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.

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 for withdrawal requests, and so we refer the reader to that EIP for more details.

Copyright

Copyright and related rights waived via CC0.