find the competition here

Context

eigenlayer ran a contests for their new slashing system in march 2025. the short summary of how eigenlayer works is you have AVSs, which are on-chain contracts for verification and an off-chain network of Operators. operators execute the service on behalf of the AVS and then post evidence of their execution on-chain to the AVS contracts.

if the operator works properly, they are rewarded, but if they misbehave they get slashed and removed from the operator set. operators have to stake ether to start working. eventually when they want to withdrawal, they have to wait MIN_WITHDRAWAL_DELAY_BLOCKS on queue, which was set to 14 days. if they have funds in the queue and get slashed, not only their active stake is slashed but the ones queued for exit too. one single operator can participate in multiple AVSs with different weights given to each. see the image below for an example.

eigenranks
AVS hierarchy

Bug

now follow me on with all the context i gave u. the way they track withdrawals is by snapshotting the current block w/ any operator action that changes the queue, so either starting or finishing a withdrawal. When the operator has to be slashed the system looks at what the snapshot looked like MIN_WITHDRAWAL_DELAY_BLOCKS ago.

the bug lies in the counterintuitive fact that for proxies, immutable variables are also upgradeable, once we presume governance WILL change MIN_WITHDRAWAL_DELAY_BLOCKS, we get a situation where if they increase it, the operator can get slashed on purpose to cause the system to go south.

let's assume the governance proposal to increase the delay from 14 to 21 days passes and its scheduled to go live on the 30th day of the month.

  1. on the 15th day, an operator queues 500 of their 1000 shares, scheduled to leave on the 29th.
  2. on the 25th day, the same operator queues another 100.
  3. on the 29th day, the operator finishes the first withdrawal and gets 500 shares back.
  4. on the 30th day, after the proposal is live, the operator misbehaves on purpose. How they do it doesn't matter. the AVS then fully (works partially too) slashes their shares as punishment, and so they'll call slashOperator() in AllocationManager.sol, where it calls slashOperatorShares() in DelegationManager.sol.
  5. inside this function, it calculates shares slashed = operator's shares - operator's shares * newMag / prevMag which results in 400 shares. all good so far.
  6. then it calculates the shares to slash that are still queued: scaledSharesSlashedFromQueue = (curQueuedScaledShares - prevQueuedScaledShares) * (prevMax - newMax) / 1e18. this should be slashing the 100 shares in the queue, but thanks to the new WITHDRAWAL_DELAY, it compares the current total cumulative shares in queue, 600, to the one 21 days prior, on the 9th, which was zero. the shares slashed from queue is calculated as 600 shares instead of 100.
  7. the operator is slashed, but the amount of shares burned are larger than whats in the queue. this excess loss is paid by the other operators in the system.
function _getSlashableSharesInQueue(
        address operator,
        IStrategy strategy,
        uint64 prevMaxMagnitude,
        uint64 newMaxMagnitude
    ) internal view returns (uint256) {
        // We want ALL shares added to the withdrawal queue in the window [block.number - MIN_WITHDRAWAL_DELAY_BLOCKS, block.number]
        //
        // To get this, we take the current shares in the withdrawal queue and subtract the number of shares
        // that were in the queue before MIN_WITHDRAWAL_DELAY_BLOCKS.
        uint256 curQueuedScaledShares = _cumulativeScaledSharesHistory[operator][strategy].latest();
        uint256 prevQueuedScaledShares = _cumulativeScaledSharesHistory[operator][strategy].upperLookup({
            key: uint32(block.number) - MIN_WITHDRAWAL_DELAY_BLOCKS - 1 // bug is here, this is NOT immutable!
        });

if weaponized, a malicious operator and/or AVS can inflate the number of funds being burned, in order to destroy deposited funds, at the cost of the stake of the operator getting slashed. the attack window is up to the difference between the new withdrawal delay and the previous one, in the example given above, 21 - 14 = 7 days. also the bug only happens if they increase the delay value.

eigenbugs
illustration of the bug

Lesson

this article, unlike the other ones, has a sad ending. back then I didn't knew platform rules straight up ruled out future upgrades oops. but the sponsor said it was a high value finding and the judges felt it wasn't fair to invalidate it. so its a low or maybe a high on a different platform, but its a damn good bug I say!

if you wanna take away something from this, know that appearences can fool you. they (the devs) will write variables as constants, write variables as immutables, but inside a proxy you can just do whatever you want. this is why i said earlier its counterintuitive. also, for your consideration, maaaaybe this bug wouldn't be invalid for a bug bounty? especially if a protocol has plans to go through an upgrade? i personally think this would be too much effort but whenevr there's a big governance upgrade you think will pass, you audit their code to catch stuff like this. iirc compound spawned a bug out of bad upgrade last year. stay sharp out there

return