Skip to content

[Security] initWithdraw allows unauthorized pegin locking (griefing/DoS) #20

Description

@metzee32

Summary

initWithdraw in Gateway.sol does not verify that msg.sender is a registered or staked operator. Any address holding pegBTC can call this function to lock a legitimate user's pegin deposit, preventing the rightful operator from processing the withdrawal.

Affected Component

  • File: src/Gateway.sol
  • Function: initWithdraw (line ~436)
  • Contract: GatewayUpgradeable

Vulnerability Details

The initWithdraw function has no access control beyond requiring the caller to hold and approve sufficient pegBTC:

function initWithdraw(bytes16 instanceId, bytes16 graphId) external {
    // ... status checks ...
    peginData.status = PeginStatus.Locked;
    _safeTransferFrom(pegBTC, msg.sender, address(this), lockAmount);
    withdrawData.operatorAddress = msg.sender;
    withdrawData.status = WithdrawStatus.Initialized;
    // ...
}

There is no check that:

  • msg.sender is a registered operator via stakeManagement
  • msg.sender has sufficient locked stake (unlike postGraphData which checks lockedStakeOf >= minStakeAmount)
  • msg.sender corresponds to the graphData.operatorPubkey for this graph

Compare with postGraphData (line 401-406) which properly validates operator registration and stake:

address operatorStakeAddress = stakeManagement.pubkeyToAddress(graphData.operatorPubkey);
if (operatorStakeAddress == address(0)) revert OperatorNotRegistered();
if (stakeManagement.lockedStakeOf(operatorStakeAddress) < minStakeAmount) revert StakeInsufficient();

Steps to Reproduce

  1. Attacker obtains pegBTC (via DEX, bridge deposit, or any means)
  2. Attacker approves Gateway to spend their pegBTC
  3. Attacker identifies a pegin in Withdrawable status with a valid posted graph
  4. Attacker calls initWithdraw(instanceId, graphId) before the legitimate operator
  5. The pegin status changes to Locked, with attacker as operatorAddress

Impact

  • Griefing/DoS on bridge withdrawals: Attacker can front-run legitimate operators and lock pegin deposits. Since proceedWithdraw requires the Bitcoin kickoff transaction to match graphData.kickoffTxid (which only the real operator can produce), the withdrawal becomes stuck.
  • Recovery requires committee intervention: Only committeeCancelWithdraw (requiring committee signatures) can unlock the pegin, adding significant delay and operational burden.
  • Cost to attacker: Only needs to hold pegBTC temporarily (returned on cancel). Effective griefing at minimal cost.
  • Repeated attacks: Attacker can repeatedly lock the same pegin after each committee cancel.

Suggested Remediation

Add operator validation to initWithdraw:

function initWithdraw(bytes16 instanceId, bytes16 graphId) external {
    GraphData storage graphData = graphDataMap[graphId];
    address operatorStakeAddress = stakeManagement.pubkeyToAddress(graphData.operatorPubkey);
    require(msg.sender == operatorStakeAddress, "Not the graph operator");
    // ... rest of function
}

Severity

Medium - Denial of service on bridge withdrawal flow. No direct fund loss, but requires committee intervention to resolve and can be repeated indefinitely.

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