Selfdestruct Usage
Detects use of the selfdestruct opcode, which can permanently destroy a contract and forcibly send its Ether balance to an arbitrary address.
Selfdestruct Usage
Overview
Remediation Guide: How to Fix Selfdestruct Usage
The selfdestruct detector identifies contracts that include the SELFDESTRUCT opcode (formerly SUICIDE). When executed, selfdestruct deletes the contract’s bytecode from the blockchain and forcibly forwards its entire Ether balance to a specified recipient address — including to contracts that have no receive() or fallback() function, bypassing normal transfer restrictions. This forced Ether delivery can break assumptions in contracts that use address(this).balance as a security invariant.
Sigvex identifies SELFDESTRUCT in decompiled bytecode across all execution paths and reports: the function(s) that can trigger it, the access control protecting the call (if any), and whether the recipient address is user-controlled. Unprotected or user-controlled selfdestruct calls receive high severity; protected calls in admin functions receive lower severity as informational findings.
Since the Cancun hard fork (EIP-6780, March 2024), SELFDESTRUCT no longer deletes contract code unless it is called in the same transaction that deployed the contract. It still forwards the Ether balance. This change reduces (but does not eliminate) the risks associated with this opcode.
Why This Is an Issue
selfdestruct creates several serious security issues:
Forced Ether injection: Any contract can have Ether forcibly sent to it via selfdestruct, even if the recipient reverts on Ether receipt. This allows an attacker to break invariants in contracts that assume address(this).balance equals their internal accounting.
Unprotected contract destruction: If selfdestruct is not properly access-controlled, an attacker who gains control of the triggering function can destroy the contract, locking any non-Ether assets (ERC-20, ERC-721) held by the contract address permanently.
Proxy pattern disruption: In upgradeable proxy systems, selfdestruct in the implementation contract can destroy the implementation while the proxy continues to forward calls to a now-deleted address. This was the mechanism behind the Parity Multi-Sig wallet freeze ($150M in locked ETH, 2017), where an attacker self-destructed the shared library contract.
Metamorphic contract misuse: selfdestruct combined with CREATE2 allows “metamorphic contracts” — contracts that can redeploy with different bytecode to the same address, undermining assumptions about address-based trust.
How to Resolve
In most modern contracts, selfdestruct can simply be removed. If emergency fund recovery is needed, use a proper withdrawal pattern:
// Before: Vulnerable — unprotected selfdestruct
contract Dangerous {
address public owner;
constructor() {
owner = msg.sender;
}
// Anyone can call this and destroy the contract
function destroy() external {
selfdestruct(payable(msg.sender));
}
}
// After Option A: Remove selfdestruct entirely, use withdrawal pattern
contract Safe {
address public owner;
constructor() {
owner = msg.sender;
}
// Emergency withdrawal without destroying the contract
function emergencyWithdraw() external {
require(msg.sender == owner, "Not owner");
payable(owner).transfer(address(this).balance);
}
}
// After Option B: If selfdestruct is required, restrict access strictly
contract AccessControlled {
address public owner;
constructor() {
owner = msg.sender;
}
function destroy() external {
require(msg.sender == owner, "Not owner");
selfdestruct(payable(owner)); // Recipient is hardcoded, not user-supplied
}
}
For contracts used as proxy implementations, never include selfdestruct in implementation contracts:
// Dangerous: implementation contract with selfdestruct
contract ImplementationV1 {
function kill() external {
// This destroys the IMPLEMENTATION — proxy calls start returning empty bytes
selfdestruct(payable(msg.sender));
}
}
// Safe: no selfdestruct in implementation contracts
contract ImplementationV1Safe {
// No destruction mechanism — upgrades handled by proxy admin only
}
Examples
Vulnerable Code
// Pattern 1: Unprotected selfdestruct
contract VulnerableToken {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
// Anyone can call this!
function shutdown() external {
selfdestruct(payable(msg.sender));
// All ERC-20 balances are now permanently inaccessible
}
}
// Pattern 2: User-controlled recipient
contract UserControlledDestruct {
address public admin;
function destroy(address payable recipient) external {
require(msg.sender == admin);
selfdestruct(recipient); // Recipient can be a contract that rejects Ether
}
}
Fixed Code
// Proper emergency mechanism without selfdestruct
contract SafeVault {
address public admin;
bool public paused;
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
function pause() external onlyAdmin {
paused = true;
}
function emergencyWithdraw(address payable to) external onlyAdmin {
require(paused, "Must pause first");
to.transfer(address(this).balance);
}
// Contract code persists — ERC-20 tokens can still be recovered
}
Sample Sigvex Output
{
"detector_id": "selfdestruct",
"severity": "high",
"confidence": 0.90,
"description": "Function shutdown() contains an unprotected SELFDESTRUCT call. Any caller can destroy the contract and force-send its Ether balance.",
"location": { "function": "shutdown()", "offset": 0x3c }
}
Detection Methodology
Sigvex scans decompiled bytecode for the SELFDESTRUCT opcode and performs the following analysis:
- Opcode identification: Locates all
SELFDESTRUCTinstructions in every function’s basic blocks. - Access control assessment: Traces control-flow predecessors to determine whether the
SELFDESTRUCTcall is guarded by anSLOAD/comparison pattern consistent with an ownership or role check. - Recipient analysis: Determines whether the Ether recipient is a constant (safer) or a runtime value from calldata or stack (higher risk).
- Proxy context: Flags contracts identified as proxy implementations at higher severity since
selfdestructin implementations is categorically dangerous. - EIP-6780 awareness: Notes the post-Cancun behavior (no code deletion unless same transaction) in findings metadata.
Limitations
False positives:
- Well-protected
selfdestructin admin-only emergency functions may be reported, though at lower confidence. - Test contracts or development utilities may legitimately include
selfdestructfor testing purposes.
False negatives:
selfdestructtriggered viadelegatecallfrom another contract will not be detected in the calling contract’s bytecode.- Dynamic dispatch patterns where the function selector is computed may evade the opcode scan.
Related Detectors
- Access Control — detects missing or weak access control on sensitive operations
- Delegatecall — detects dangerous delegatecall patterns including proxy implementation risks