Skip to main content
Sigvex

Unprotected Ether Withdrawal

Detects functions that transfer Ether without verifying the caller's authorization, allowing anyone to drain the contract's ETH balance.

Unprotected Ether Withdrawal

Overview

The unprotected ether withdrawal detector identifies functions that send ETH (via CALL with non-zero value, transfer, or send) without checking that msg.sender matches a privileged address. Any user who discovers such a function can drain the contract’s entire ETH balance.

Sigvex locates CALL instructions with non-zero value operands and checks whether the function contains a CALLER comparison against a stored address before the call.

Why This Is an Issue

This is the simplest form of access control failure: a public function exists that sends ETH to msg.sender or to an address parameter, without any ownership or role check. The Parity multi-sig wallet freeze involved a similar pattern where critical functions lacked access control.

How to Resolve

// Before: Vulnerable — anyone can withdraw
function withdraw() external {
    payable(msg.sender).transfer(address(this).balance);
}

// After: Fixed — restrict to owner
modifier onlyOwner() {
    require(msg.sender == owner, "Not owner");
    _;
}

function withdraw() external onlyOwner {
    payable(owner).transfer(address(this).balance);
}

Detection Methodology

  1. ETH transfer detection: Identifies CALL instructions where the value parameter is non-zero, plus SELFDESTRUCT which forwards remaining balance.
  2. Authorization check search: Looks for CALLER (msg.sender) loaded and compared (EQ) against a storage value before the transfer.
  3. Destination analysis: Checks whether the ETH destination is msg.sender (higher risk) or a hardcoded address (lower risk).

Limitations

False positives: Functions protected by access control in a separate modifier or parent contract may be flagged if the check is not inlined. False negatives: Contracts that use assembly-level balance manipulation may be missed.

References