Skip to content

fix(staking): close slash exit-dodge via the unbonding queue#19

Merged
RunTimeAdmin merged 1 commit into
masterfrom
fix/slash-exit-dodge
Jul 22, 2026
Merged

fix(staking): close slash exit-dodge via the unbonding queue#19
RunTimeAdmin merged 1 commit into
masterfrom
fix/slash-exit-dodge

Conversation

@RunTimeAdmin

Copy link
Copy Markdown
Owner

Summary

Closes a HIGH-severity gap that let an agent operator escape slashing entirely, defeating the protocol's core "accountability that costs something" guarantee.

initiateSlash gated only on the active stake (stakes[didHash].amount). But slashable funds can all sit in the unbonding queue (unbondingAmount), and initiateWithdrawal let an operator move their entire stake there. Once the active balance hit zero, initiateSlash reverted NoStake, so no proposal could be created, executeSlash never ran, and the operator claimed the full stake after the unbonding period. The existing claimWithdrawal / executeSlash protections never fired, because they all assume a slash can first be initiated.

Attack

  1. Operator with 1,000 staked misbehaves.
  2. Before the committee initiates a slash, the operator calls initiateWithdrawal(didHash, 1000). Active stake is now 0, all queued.
  3. Committee calls initiateSlash and it reverts NoStake.
  4. After the unbonding period, claimWithdrawal returns the full 1,000. Slash dodged.

The operator reliably wins the race: the withdrawal is one instant transaction, while committee initiation (multisig, evidence gathering) is not.

Fix

  • initiateSlash now gates on the total slashable balance (amount + unbondingAmount), so a slash can be initiated against fully-queued stake. claimWithdrawal then blocks (pending slash) and executeSlash sweeps the queue as it already did.
  • initiateWithdrawal no longer lets an Active agent drain to zero in one step (removed the remaining != 0 carve-out). Full exit requires suspending first, matching the documented intent and keeping an active agent always backed. Defense-in-depth on top of the primary fix.

Tests

  • test_exitDodge_slashStillInitiableWhenFullyQueued: a slash stays initiable and executable against a fully-queued stake, funds are swept, and the operator cannot claim.
  • test_initiateWithdrawal_reverts_toZeroWhileActive: Active withdraw-to-zero reverts.
  • Full suite: 171 passing, 0 failing (up from 169). No existing test changed behavior; test_initiateSlash_reverts_noStake still correctly reverts for a genuinely stakeless agent.

Scope note

CountersigOracleBond is not affected: its bond stays in a single op.bond field through Exiting, and slash() operates on it regardless of status, so there is no "funds moved somewhere slash cannot reach." The issue was specific to CountersigStaking's split active/unbonding accounting.

Found during an internal adversarial review of the stake-exit and slash lifecycle. This was a read-level pass over two contracts and is not a substitute for a full audit before mainnet.

🤖 Generated with Claude Code

initiateSlash gated only on the active stake (s.amount), so an operator could
move their entire stake into the unbonding queue and make the slash impossible
to even initiate (NoStake), despite the queued funds being nominally slashable.
The claimWithdrawal/executeSlash defenses never fired because no proposal could
be created. This nullified slashing, the protocol's core accountability.

- initiateSlash now gates on the total slashable balance (amount + unbondingAmount),
  so a slash can be initiated against fully-queued stake; claimWithdrawal then
  blocks (pending slash) and executeSlash sweeps the queue as before.
- initiateWithdrawal no longer lets an Active agent drain to zero in one step
  (removed the remaining != 0 carve-out); full exit requires suspending first,
  matching the documented intent and keeping an active agent always backed.
- Regression tests: slash stays initiable/executable against a fully-queued
  stake; Active withdraw-to-zero reverts. Full suite 171 passing.

Found during an internal adversarial review of the stake-exit/slash lifecycle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 22, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@RunTimeAdmin
RunTimeAdmin merged commit 7869bdd into master Jul 22, 2026
3 checks passed
@RunTimeAdmin
RunTimeAdmin deleted the fix/slash-exit-dodge branch July 22, 2026 14:58
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