Summary
trySlash in StakingLib returns false without reverting when a validator's effective balance is zero, which occurs when their stake was in the GSE bonus bucket and a newer rollup has been registered
- The caller (
Slasher.slash via SlashPayload actions) only checks that the call succeeded, not the return value, so the slash round is marked as executed without actually slashing anyone
- In practice this means a slash voted before a rollup upgrade can silently fail to execute if the upgrade lands first — the validator keeps their full stake and nothing indicates the round was a no-op
- Instead, revert so the failure surfaces and the round can be investigated or retried
Root cause
When a validator deposits with moveWithLatestRollup = true, their stake goes to the GSE bonus bucket. effectiveBalanceOf gates bonus-bucket access on getLatestRollup() == _instance (GSE.sol). Once a newer rollup is registered via addRollup, the old rollup is no longer "latest" and the lookup returns 0 for all bonus-bucket validators.
If a slash round on the old rollup executes after migration, trySlash hits the !isSlashable path (StakingLib.sol:267) and returns false. Since Slasher.slash only requires that the external call didn't revert (Slasher.sol:87-92), a false return is indistinguishable from success — the round completes silently.
Patch direction
Option A: revert in trySlash when the effective balance is zero, instead of returning false. This makes the failure visible at the round level.
Option B: widen the effectiveBalanceOf gate to let recently-superseded rollups still reach the bonus bucket during a grace window (e.g. 30 days), preserving slashing jurisdiction long enough for pending rounds to complete.
This issue was identified with the assistance of AI tooling during code review.
Summary
trySlashinStakingLibreturnsfalsewithout reverting when a validator's effective balance is zero, which occurs when their stake was in the GSE bonus bucket and a newer rollup has been registeredSlasher.slashviaSlashPayloadactions) only checks that the call succeeded, not the return value, so the slash round is marked as executed without actually slashing anyoneRoot cause
When a validator deposits with
moveWithLatestRollup = true, their stake goes to the GSE bonus bucket.effectiveBalanceOfgates bonus-bucket access ongetLatestRollup() == _instance(GSE.sol). Once a newer rollup is registered viaaddRollup, the old rollup is no longer "latest" and the lookup returns 0 for all bonus-bucket validators.If a slash round on the old rollup executes after migration,
trySlashhits the!isSlashablepath (StakingLib.sol:267) and returnsfalse. SinceSlasher.slashonly requires that the external call didn't revert (Slasher.sol:87-92), afalsereturn is indistinguishable from success — the round completes silently.Patch direction
Option A: revert in
trySlashwhen the effective balance is zero, instead of returningfalse. This makes the failure visible at the round level.Option B: widen the
effectiveBalanceOfgate to let recently-superseded rollups still reach the bonus bucket during a grace window (e.g. 30 days), preserving slashing jurisdiction long enough for pending rounds to complete.This issue was identified with the assistance of AI tooling during code review.