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
- ETH transfer detection: Identifies
CALLinstructions where the value parameter is non-zero, plusSELFDESTRUCTwhich forwards remaining balance. - Authorization check search: Looks for
CALLER(msg.sender) loaded and compared (EQ) against a storage value before the transfer. - 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.
Related Detectors
- Access Control — detects missing authorization broadly
- Selfdestruct — detects selfdestruct which also transfers ETH