Skip to content

Permissionless initWithdraw plus untimed committeeCancelWithdraw lets any pegBTC holder repeatedly lock a victim's withdrawal #24

Description

@mr-hrydarie

Permissionless initWithdraw plus untimed committeeCancelWithdraw lets any pegBTC holder repeatedly lock a victim's withdrawal

Summary

initWithdraw in Gateway.sol has no operator or victim check. Any address holding pegBTC equal to a victim's pegin amount can call it against the victim's instance, which moves the instance to Locked and blocks the victim's withdrawal until the committee signs committeeCancelWithdraw. That function has no timelock and refunds the caller in full, so the attacker's capital returns and the cycle can repeat. A second defect compounds this: the operator's self-service cancelWithdraw is commented out, so once locked there is no exit path that does not require committee action.

Details

  • Type: Business logic griefing with capital lock
  • Asset: GOAT BitVM3 L2 gateway, repository bitvm-L2-contracts, commit 89dbee2
  • Location: src/Gateway.sol: initWithdraw lines 428 to 457, commented-out cancelWithdraw lines 459 to 473, committeeCancelWithdraw lines 475 to 489
  • Auth required: none for the attacker beyond owning pegBTC matching the victim's pegin amount

Relation to issues #20 and #22 (differentiation)

Issues #20 and #22 already report that initWithdraw lacks access control, including the lock-out effect on the victim. This report adds what neither covers:

  1. The repeat loop economics. committeeCancelWithdraw has no timelock and refunds the attacker's lockAmount in full. The attack therefore costs gas plus temporary capital lockup, and the same victim or any other one can be re-locked immediately after each cancellation. Issues [Security] initWithdraw allows unauthorized pegin locking (griefing/DoS) #20/initWithdraw` smart contract function lacks access control — LOW/MEDIUM #22 describe a single-shot lockout; this is an indefinite, cheaply renewable one.
  2. The missing self-service exit. cancelWithdraw, the operator's own cancel with its timelock (btcBlockHeightAtWithdraw + cancelWithdrawTimelock < block.height), is commented out at lines 459 to 473. Once locked, only the committee can unwind.

Suggested handling: treat as additional impact on the existing reports if you prefer, but the timelock omission in committeeCancelWithdraw is a distinct line of code needing its own fix.

Attack path

  1. Victim pegs in; their pegin reaches status Withdrawable and the committee posts the graph.
  2. Attacker calls initWithdraw(victimInstanceId, victimGraphId). Requirements pass: status is None or Canceled, and graphData.peginTxid == peginData.peginTxid. The attacker's lockAmount transfers to the gateway and the victim's instance becomes Locked.
  3. The victim cannot withdraw. Only the committee can unwind.
  4. Attacker requests cancellation; committeeCancelWithdraw refunds the full lockAmount and resets the instance to Withdrawable.
  5. Repeat from step 2 each time the victim retries.

Impact

Shown from source:

  • Single-shot griefing is unconditional: one call locks the victim's withdrawal until committee intervention.
  • Attacker capital is fully refunded on cancel, so the repeated attack costs gas plus temporary capital lockup.
  • The repeated loop depends on the committee signing cancellations. If it refuses, attacker and victim stay mutually locked. This is griefing with mutual lock, not theft: no path moves victim funds to the attacker.
  • Blast radius is per-instance, not bridge-wide.

Not shown: no fork test was executed; this finding rests on line-cited source review across two passes rather than an executed PoC.

Severity requested: Medium. Per-victim denial of service with recoverable state and no fund loss; bounded below High by the 1:1 capital requirement and the committee-dependent loop.

Remediation

  1. Bind initWithdraw to the graph operator: require msg.sender == graphData.operatorAddress.
  2. Add a per-instance cooldown after any cancel so the legitimate party wins the retry race.
  3. Restore the commented-out cancelWithdraw with its timelock so operators have a self-service exit, and consider a penalty for repeated cancel cycles.

Submitted as part of the GOAT BitVM3 bug bounty campaign (Aug 18 to Sep 18, 2026). Contact via GitHub or Tally submission under the same name.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions