You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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:
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
Victim pegs in; their pegin reaches status Withdrawable and the committee posts the graph.
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.
The victim cannot withdraw. Only the committee can unwind.
Attacker requests cancellation; committeeCancelWithdraw refunds the full lockAmount and resets the instance to Withdrawable.
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
Bind initWithdraw to the graph operator: require msg.sender == graphData.operatorAddress.
Add a per-instance cooldown after any cancel so the legitimate party wins the retry race.
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.
Permissionless initWithdraw plus untimed committeeCancelWithdraw lets any pegBTC holder repeatedly lock a victim's withdrawal
Summary
initWithdrawin 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 toLockedand blocks the victim's withdrawal until the committee signscommitteeCancelWithdraw. 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-servicecancelWithdrawis commented out, so once locked there is no exit path that does not require committee action.Details
89dbee2src/Gateway.sol:initWithdrawlines 428 to 457, commented-outcancelWithdrawlines 459 to 473,committeeCancelWithdrawlines 475 to 489Relation to issues #20 and #22 (differentiation)
Issues #20 and #22 already report that
initWithdrawlacks access control, including the lock-out effect on the victim. This report adds what neither covers:committeeCancelWithdrawhas no timelock and refunds the attacker'slockAmountin 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.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
committeeCancelWithdrawis a distinct line of code needing its own fix.Attack path
Withdrawableand the committee posts the graph.initWithdraw(victimInstanceId, victimGraphId). Requirements pass: status is None or Canceled, andgraphData.peginTxid == peginData.peginTxid. The attacker'slockAmounttransfers to the gateway and the victim's instance becomesLocked.committeeCancelWithdrawrefunds the fulllockAmountand resets the instance toWithdrawable.Impact
Shown from source:
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
initWithdrawto the graph operator: requiremsg.sender == graphData.operatorAddress.cancelWithdrawwith 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.