Skip to main content
Sigvex

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:

  1. Contract has a kill(address payable recipient) function with no onlyOwner modifier.
  2. Attacker calls kill(attackerAddress) directly.
  3. Contract balance (100 ETH) is transferred to the attacker.
  4. Contract bytecode is erased. All future calls to the contract address are no-ops.
  5. Any contracts that depend on this address stop functioning.

Library selfdestruct (Parity-style):

  1. A proxy wallet calls a shared WalletLibrary via DELEGATECALL.
  2. The library has initWallet(address) with an initialized guard — but the guard only applies in the library’s own storage, not in the proxy’s storage.
  3. The attacker calls initWallet(attackerAddress) directly on the library (not through a proxy).
  4. The library’s initialized flag is false (it is only set in proxy storage during legitimate use), so the call succeeds.
  5. The attacker is now owner in the library’s storage.
  6. The attacker calls kill(attackerAddress) on the library.
  7. The library is destroyed. All 587 proxy wallets now DELEGATECALL into empty code and all their functions permanently revert.

Forced ETH reception (balance invariant break):

  1. A victim contract uses require(address(this).balance == totalDeposits) as an invariant.
  2. The attacker deploys ForcedEtherExploit and funds it with 1 ETH.
  3. The attacker calls attack(), which calls selfdestruct(payable(target)).
  4. SELFDESTRUCT force-sends ETH to the target, bypassing receive() and fallback().
  5. address(target).balance is now greater than totalDeposits.
  6. 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

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

References