Selfdestruct Vulnerability Exploit Generator
Sigvex exploit generator that validates selfdestruct vulnerabilities including unprotected self-destruction, library-based destruction (Parity-style), and forced ETH reception.
Selfdestruct Vulnerability Exploit Generator
Overview
The selfdestruct vulnerability exploit generator validates findings where the SELFDESTRUCT opcode is reachable without sufficient access control. The generator identifies which sub-type of selfdestruct vulnerability applies — unprotected destruction, library destruction (the Parity pattern), or forced ETH reception — and produces a targeted proof of concept for each.
The canonical historical example is the Parity Wallet hack #2 (November 2017), where an attacker initialized the shared WalletLibrary contract directly (bypassing the proxy), became its owner, and called kill(). This destroyed the shared library, rendering 587 dependent wallets permanently non-functional and freezing 513,774 ETH (~$150M at the time) forever.
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
The generator classifies the vulnerability from the finding’s description field and runs the matching attack path:
Unprotected selfdestruct:
- Contract has a
kill(address payable recipient)function with noonlyOwnermodifier. - Attacker calls
kill(attackerAddress)directly. - Contract balance (100 ETH) is transferred to the attacker.
- Contract bytecode is erased. All future calls to the contract address are no-ops.
- Any contracts that depend on this address stop functioning.
Library selfdestruct (Parity-style):
- A proxy wallet calls a shared
WalletLibraryviaDELEGATECALL. - The library has
initWallet(address)with aninitializedguard — but the guard only applies in the library’s own storage, not in the proxy’s storage. - The attacker calls
initWallet(attackerAddress)directly on the library (not through a proxy). - The library’s
initializedflag is false (it is only set in proxy storage during legitimate use), so the call succeeds. - The attacker is now
ownerin the library’s storage. - The attacker calls
kill(attackerAddress)on the library. - The library is destroyed. All 587 proxy wallets now
DELEGATECALLinto empty code and all their functions permanently revert.
Forced ETH reception (balance invariant break):
- A victim contract uses
require(address(this).balance == totalDeposits)as an invariant. - The attacker deploys
ForcedEtherExploitand funds it with 1 ETH. - The attacker calls
attack(), which callsselfdestruct(payable(target)). SELFDESTRUCTforce-sends ETH to the target, bypassingreceive()andfallback().address(target).balanceis now greater thantotalDeposits.- The invariant check fails permanently, locking all withdrawals.
Exploit Mechanics
The generator parses the finding description to select a vulnerability type:
| Description contains | Selected type | Severity |
|---|---|---|
| ”library” or “delegatecall” | library_selfdestruct | Critical: can freeze all dependent contracts |
| ”unprotected” or “access” | unprotected_selfdestruct | High: direct fund theft |
| ”force” | forced_ether_reception | Medium: invariant break |
| (other) | general_selfdestruct | High |
The generator uses ExploitTestResult::success with an estimated 50,000 gas and attaches vulnerability_type to the evidence map. The PoC is a complete Solidity demonstration covering all three patterns.
The Parity-style attack is demonstrated as:
contract ParityStyleExploit {
WalletLibrary public targetLibrary;
function attack() public {
// Step 1: Initialize library directly (not through proxy)
targetLibrary.initWallet(address(this));
require(targetLibrary.owner() == address(this));
// Step 2: Destroy the library
targetLibrary.kill(payable(msg.sender));
// Result: 587 wallet proxies permanently broken
}
}
Remediation
- Detector: Selfdestruct Detector
- Remediation Guide: Selfdestruct Remediation
The recommended approach is to avoid SELFDESTRUCT entirely. As of EIP-6049, SELFDESTRUCT is deprecated and future Ethereum hard forks may alter or remove its functionality.
// INSTEAD OF selfdestruct: use emergency withdrawal + lock
contract SecureContract {
address public owner;
bool public locked;
modifier onlyOwner() { require(msg.sender == owner); _; }
function emergencyWithdraw() public onlyOwner {
locked = true; // prevent future operations
payable(owner).transfer(address(this).balance);
}
}
// For libraries: NEVER include selfdestruct
// Libraries should be stateless and immutable
For the balance invariant issue, use internal accounting instead of address(this).balance:
// INSTEAD OF: require(address(this).balance == totalDeposits)
// USE:
mapping(address => uint256) public balances;
// Only track what went through deposit(), not raw ETH balance