When Slashing Breaks: Anatomy of ERC4626 Restaking Vulnerabilities

Restaking protocols promise a simple security model: operators stake ETH, distributed services (DSS) rely on that stake as collateral, and if an operator misbehaves, the DSS slashes a portion of their stake. The mechanism is straightforward in theory. In practice, audit after audit turns up slashing implementations that are partially or entirely non-functional.

This post walks through the failure modes I see repeatedly in restaking protocol audits โ€” specifically around native ETH vaults built on ERC4626. The pattern is almost always the same: accounting and movement are implemented in separate functions, tested independently, but never traced end-to-end to verify that ETH actually moves when a slash is finalized.

The ERC4626 Restaking Model

ERC4626 vaults are the standard primitive for tokenized yield-bearing positions. In a restaking context, a user deposits ETH into a NativeVault, receives shares representing their stake, and those shares are the collateral that DSS operators commit. When a slash occurs, the protocol should reduce the vault's underlying ETH โ€” diluting all shareholders pro-rata and transferring the slashed amount to a configured handler.

The key property is that slashing must be economic, not just accounting. Marking a vault as "slashed by 10%" in storage is not slashing. ETH must physically leave the vault.

Failure Mode 1: The No-Op Slash

The most common failure: slashAssets() is implemented but only updates internal accounting. No ETH moves.

Solidity โ€” slashAssets as a no-op
function slashAssets(uint256 totalAssetsToSlash, address slashingHandler)
    external override onlyCore
    returns (uint256 transferAmount)
{
    // Computes how much to reduce from the accounting snapshot
    uint256 snapshotSlash = Math.min(totalAssetsToSlash, totalAssets());
    _slashSnapshot(snapshotSlash);  // updates internal storage

    // Returns the amount โ€” but never actually transfers it
    return snapshotSlash;
    // slashingHandler is never called
    // No ETH moves
}

The function signature looks correct โ€” it takes a slashingHandler parameter, which implies it intends to route funds somewhere. It returns the slash amount, which the caller can log or emit. But the handler is never invoked.

Why does this happen? Usually because the accounting update is implemented first and tested (the tests pass โ€” shares are diluted correctly), then the physical transfer is deferred as a "later step" that never lands. Sometimes the developer assumes the caller (Core.sol or equivalent) will do the transfer after calling slashAssets โ€” but Core never does.

The result: operators' on-paper stake decreases, but their actual ETH is untouched. The DSS believes it has slashed collateral; the operator keeps the funds.

Failure Mode 2: The address(0) Drain

The second failure is rarer but more severe: ETH does move, but it goes to address(0) rather than the configured handler. This is destructive โ€” the ETH is permanently unrecoverable.

Solidity โ€” _burnSlashed sending to wrong recipient
function _burnSlashed(uint256 amount) internal {
    // Intended: send to slashingHandler
    // Actual: sends to address(0)
    (bool success,) = address(0).call{value: amount}("");
    if (!success) revert SlashingFailed();
}

This typically emerges from a refactor: an early implementation burned ETH to address(0) as a placeholder during development, and the recipient was never updated to the actual handler address. The code compiles, the ETH moves, the tests pass (ETH leaves the vault). But it goes nowhere useful.

Combined with Failure Mode 1, the end state for a protocol with both bugs is:

requestSlashing() โ†’ records slash intent
... veto window passes ...
finalizeSlashing() โ†’ calls slashAssets()
slashAssets() โ†’ accounting updated (shares diluted)
โ†’ ETH does NOT move (no-op)
(eventually) _burnSlashed()โ†’ ETH moves...
โ†’ ...to address(0)
Result: operator loses accounting, keeps ETH OR ETH is destroyed

Prior audits on this type of protocol often catch the address(0) issue in isolation (it's visible as a hardcoded zero address). What they miss is the compound effect with the no-op: even fixing _burnSlashed doesn't help if slashAssets never calls it.

Failure Mode 3: The Sandwich

A subtler failure doesn't involve broken code at all โ€” it's a timing property. If the slash percentage is fixed at requestSlashing time but the asset amount is computed at finalizeSlashing time, sophisticated stakers can reduce their exposure by front-running finalization.

Solidity โ€” slash amount computed at finalization (vulnerable)
// Called at finalization, not at request time
function computeSlashAmount(address vault, uint256 slashPercentageWad)
    internal view
    returns (uint256)
{
    // totalAssets() is live โ€” reflects current vault state
    return Math.mulDiv(slashPercentageWad, IKarakBaseVault(vault).totalAssets(), MAX_SLASHING_PERCENT_WAD);
}

Between requestSlashing and finalizeSlashing there's typically a 2-day veto window. A staker watching the mempool can see the pending slash and call startRedeem to queue a withdrawal. This doesn't immediately reduce totalAssets(), but it begins the 9-day withdrawal delay. By the time slashing finalizes, the staker's position is partly decoupled from the vault's asset pool.

The fix is to snapshot totalAssets() at requestSlashing time and use that snapshot value at finalization, regardless of how much has moved into withdrawal queues in the interim.

Failure Mode 4: First-Depositor Inflation

This is a known ERC4626 issue that restaking protocols frequently reintroduce by not implementing virtual shares. When a vault has zero shares outstanding, the first depositor controls the share price. A sophisticated attacker can:

  1. Front-run a victim's deposit with a 1-wei first deposit (receives 1 share)
  2. Donate a large amount of ETH directly to the vault (totalAssets grows, shares unchanged)
  3. Victim's deposit is rounded down to 0 shares (or very few), suffering a near-total loss
  4. Attacker redeems their 1 share for almost all vault assets

The standard fix is virtual shares: initialize the share supply with a non-zero amount (e.g., 10**decimals() shares assigned to address(0)) so the attacker's rounding manipulation becomes economically infeasible.

Solidity โ€” virtual shares protection
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256) {
    // Virtual shares: add 1 to both numerator and denominator
    return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding);
}

The End-to-End Test Protocol

Most test suites for restaking protocols test slashing by checking that share values decrease after a slash. This passes even when ETH never moves โ€” accounting updates correctly, the share price drops, the math checks out.

The test that catches these bugs is simpler and more direct: track the balance of the slashing handler before and after a full slash cycle, and verify it increases by the expected amount.

Test pattern โ€” verify ETH actually arrives
function test_slashingMovesETH() public {
    uint256 handlerBalanceBefore = slashingHandler.balance;
    uint256 vaultAssetsBefore = nativeVault.totalAssets();

    // Full slash cycle
    core.requestSlashing(operator, dss, slashPercentage);
    vm.warp(block.timestamp + SLASHING_VETO_WINDOW + 1);
    core.finalizeSlashing(slashRequestId);

    uint256 expectedSlash = vaultAssetsBefore * slashPercentage / MAX_SLASH_PERCENT;
    uint256 handlerBalanceAfter = slashingHandler.balance;

    // This is the test that matters
    assertApproxEqAbs(
        handlerBalanceAfter - handlerBalanceBefore,
        expectedSlash,
        1e9  // small tolerance for rounding
    );
}

If this test passes, slashing works. If it fails, there's a physical movement bug regardless of what the accounting shows. This test is conspicuously absent from most restaking protocol test suites.

Why Restaking Is a High-Value Audit Target

ERC4626 vaults are audited constantly. The base spec is well-understood, and auditors have strong pattern recognition for share inflation, rounding errors, and reentrancy. Restaking adds a layer above the vault โ€” slashing logic, DSS coordination, operator registration โ€” and that layer is consistently under-audited compared to the vault primitives it wraps.

The slashing mechanism is also the highest-stakes component: it's the enforcement mechanism that makes the entire security model credible. If it doesn't work, restaked ETH doesn't actually provide security guarantees to DSS operators. The protocol promises penalties for misbehavior but can't deliver them.

The core lesson: When auditing slashing in restaking protocols, don't trust accounting tests. Trace every code path from requestSlashing to finalizeSlashing and ask a single question: does the handler's balance increase? If you can't answer "yes" with a concrete test, the slashing mechanism is broken.
ยท ยท ยท

This post describes vulnerability classes found during independent security audits of restaking protocols. All findings have been responsibly disclosed to the relevant teams. No protocol names are disclosed while responses are pending.