High severity

Unprotected selfdestruct

selfdestruct (formerly suicide) sends a contract's entire Ether balance to an address and, in the same step, can delete the contract's code and storage. That is a legitimate decommissioning tool — but only the right party should be able to pull it. When a function containing selfdestruct has no access control, anyone can call it: they sweep the contract's whole balance to an address of their choosing and, where the contract is still destroyable, brick it so every other caller's interaction reverts. This is the bug that froze the Parity multisig library: a shared logic contract with an unprotected kill path was destroyed by a stranger, locking the wallets that delegatecalled into it. The usual shape is a kill, close, destroy or “migrate” helper that reaches selfdestruct without first checking msg.sender.

Vulnerable pattern

A minimal contract with the bug
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Wallet {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    receive() external payable {}

    // No access control. Any caller can run this, sending the contract's
    // whole balance to an address they pass in and (where the contract is
    // still destroyable) wiping its code and storage.
    function kill(address payable to) external {
        selfdestruct(to);
    }
}

The fix

Gate every path that reaches selfdestruct behind access control — an onlyOwner or role check, ideally behind a timelock — and send the balance to a trusted address, not a caller-supplied one. Often the safest change is to drop selfdestruct altogether and recover funds with an owner-only withdraw instead, since a destroyed contract can break integrators that still hold its address. Note that since EIP-6780 (the Cancun upgrade) selfdestruct only deletes code and storage when it runs in the same transaction that created the contract; outside that case it now just transfers the balance. Even so, an unprotected selfdestruct still lets any caller drain the contract's entire Ether balance, and the create-and-destroy case still fully bricks it, so the bug stays high severity.

The same contract, corrected
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Wallet {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    receive() external payable {}

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    // Only the owner can decommission the contract, and the balance goes
    // to the owner, not to a caller-chosen address.
    function close() external onlyOwner {
        selfdestruct(payable(owner));
    }
}

Scan your contract for this

pactlint flags suicidal and other recurring DeFi bug classes in Solidity source or a verified contract on Ethereum or Base. Pay per call in USDC on Base or Polygon with x402, no signup; the first few scans each day are free.

How to scan →

An unprotected selfdestruct is detected by Slither's stock suicidal detector, rated high impact at high confidence, which pactlint surfaces at high severity — it keeps Slither's own impact-and-confidence rating, with no extra downgrade or triage. Run it in CI with the pactlint GitHub Action. Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.