Skip to content

Pad validator withdrawals with a reward accrual buffer - #846

Open
cyc60 wants to merge 6 commits into
masterfrom
dynamic-withdrawal-buffer
Open

cyc60 wants to merge 6 commits into
masterfrom
dynamic-withdrawal-buffer

Conversation

@cyc60

@cyc60 cyc60 commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Description

The operator has been sending withdrawValidators transactions for dust amounts (61 gwei, 62 gwei, 3954 gwei) roughly every 12 hours on the Serenita vault, each costing more in gas than it withdraws.

Root cause: the vault exit queue is share-denominated, so queued positions keep accruing rewards at every oracle update. The operator requested exactly the getExitQueueMissingAssets shortfall, floored to 1 gwei and truncated down. An EL-triggered partial withdrawal only lands after MIN_VALIDATOR_WITHDRAWABILITY_DELAY (256 epochs, ~27h on mainnet) plus its wait in the pending-partials sweep plus the next harvest, so every reward update during that window produced a fresh shortfall of ~12h of rewards on the remaining queue, and the operator immediately chased it with another transaction.

Changes:

  • get_queued_assets now returns ExitQueueAssets(missing, total). missing is the net shortfall as before. total is the same checker call with in-flight withdrawals and redemptions set to zero, i.e. the whole remaining queue that keeps accruing rewards.
  • New calculate_withdrawal_buffer pads the request with the rewards expected to accrue on total over the full latency window: the larger of the withdrawability delay and the pending-partials sweep wait (from the queue length the operator already fetches, the sweep drains concurrently with the delay), plus the keeper rewardsDelay, using the osToken controller avgRewardPerSecond as the rate and a 2x safety factor. Excess lands as withdrawable assets and is re-staked by the normal funding path.
  • The buffer is applied only on the partial-withdrawal path; the partial-vs-full decision still uses the unbuffered shortfall, so the buffer can never force a full validator exit.
  • MIN_WITHDRAWAL_AMOUNT_GWEI becomes an env setting (default 1 gwei, so any positive shortfall is served and a small exit is paid once rather than left in the queue). A startup check caps it at 0.01 ETH, mirroring the oracle's MISSING_ASSETS_THRESHOLD: above that the operator would skip shortfalls the oracle already treats as exit-worthy and covers with a full validator exit.
  • NetworkConfig gains MIN_VALIDATOR_WITHDRAWABILITY_DELAY_EPOCHS (256 on all networks) and MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP (8 on mainnet and hoodi, 6 on gnosis), verified against live consensus node specs.
  • New contract wrappers KeeperContract.rewards_delay and OsTokenVaultControllerContract.avg_reward_per_second; get_withdrawals_count is fetched once per run and reused.
  • The second checker call for total is skipped when there is no shortfall. In the full-exit path the partial top-up is skipped once a full exit covers the shortfall, so the buffer alone never produces a partial request.

For a 120 ETH queue at the current ~2% protocol rate the buffer is about 0.022 ETH; for a 164 gwei remainder it is zero.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants