neo-devpack-solidity 0.22.0

Production-focused Solidity-to-NeoVM compilation system
Documentation
---
title: "EIP-6780 — SELFDESTRUCT Restriction"
description: "EIP-6780 — SELFDESTRUCT Restriction mapped to Neo N3."
---

# EIP-6780 — SELFDESTRUCT Restriction

[Back to Protocol-Level EIPs](/standards-mirror/protocol-eips)

<StandardsMirror>

<StandardEntry
  id="eip-6780"
  title="EIP-6780 — SELFDESTRUCT Restriction"
  eip="6780"
  status="Final"
  neoMapping="ContractManagement.Destroy (explicit)"
  category="Lifecycle"
  parityLabel="Native"
  parityClass="sm-pill-native"
>

<template #spec>

## EIP-6780: Reduced SELFDESTRUCT Functionality

`SELFDESTRUCT` originally:
1. Destroyed the contract's bytecode and storage.
2. Forwarded the contract's ETH balance to a designated recipient.
3. Refunded gas to the caller (incentive to clean up unused contracts).

This created multiple footguns: re-deploying via CREATE2 to the same address with
different code (because storage was wiped), unintended data loss in proxy contracts
when the implementation called `selfdestruct`, MEV opportunities around
"refund the gas" mechanics.

EIP-6780 (Cancun) reduced `SELFDESTRUCT` to:
- **If called in the same transaction as the contract was created**: full original
  behavior (destroy + forward funds + refund gas).
- **Otherwise**: only forward the balance. The bytecode and storage stay intact.

In effect, `SELFDESTRUCT` is now a **balance-sweep** opcode for already-deployed
contracts, not a destruction primitive.

### Neo Equivalent

Neo's `ContractManagement.Destroy()` has always been:
- An explicit method call (no opcode).
- Authorized by the contract's witness.
- Removes the contract's storage and manifest.
- Has no automatic fund-forwarding or gas refund.

The "destroy" path is intentionally narrow: contracts must explicitly opt in by
implementing a callable destroy method, witnesses must check, and any token
balances held at the contract address must be transferred out beforehand by
contract logic — there's no implicit sweep.


::: tip Live on Neo TestNet
Both implementations are deployed on Neo N3 TestNet (network magic `894710606`).

| Implementation | Contract Hash | Deploy Tx |
| --- | --- | --- |
| **Solidity** (`neo-solc`) | `0x67a59c179448a3769d5559a691a4c118c7de9930` | [`0x35d83590…980e4e`]https://dora.coz.io/transaction/neo3/testnet/0x35d83590e411100bb14e15567994ba516f101e8c82eaea055d62cd458b980e4e |
| **Neo C#** (`nccs`) | `0x8acf52e9a1f696965480cd40046c9c3de020f8eb` | [`0x5bf67208…6d1473`]https://dora.coz.io/transaction/neo3/testnet/0x5bf67208dbb2e76f5bf9c1e48314d4b6d96f4eb979bc423d16f6be9a606d1473 |

Checked-in snapshot: Solidity 1 / 2 assertions pass; Neo C# 2 / 2 assertions pass. This is a validation snapshot, not an all-green parity certification; see [TestNet Results](/standards-mirror/deployments/RESULTS) for failure details.

Source pairs: [`docs/standards-mirror/deployments/eip-6780/`](https://github.com/r3e-network/neo-devpack-solidity/tree/main/docs/standards-mirror/deployments/eip-6780).
:::

</template>

<template #solidity>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract OldStyle {
    function destroy(address payable recipient) external {
        // Pre-Cancun: this destroyed the bytecode AND swept ETH.
        // Post-EIP-6780: only sweeps ETH unless this is a same-tx-deploy contract.
        // A surprising change for many existing contracts.
        selfdestruct(recipient);
    }
}

contract Sweeper {
    /// New idiomatic pattern: explicit balance transfer instead of selfdestruct.
    function sweep(address payable recipient) external {
        recipient.transfer(address(this).balance);
    }
}
```

</template>

<template #csharp>

```csharp
using System;
using System.ComponentModel;
using Neo;
using Neo.SmartContract.Framework;
using Neo.SmartContract.Framework.Attributes;
using Neo.SmartContract.Framework.Native;
using Neo.SmartContract.Framework.Services;

namespace R3E.Examples;

[DisplayName("DestroyableContract")]
[ContractPermission("*", "*")]
public class DestroyableContract : SmartContract
{
    private static readonly byte[] OwnerKey = { 0xff };

    [DisplayName("Destroyed")]
    public static event Action<UInt160> OnDestroyed;

    /// <summary>
    /// NEP-31 destroy. Explicit, witness-gated, removes storage and manifest.
    /// No automatic fund forwarding — handle that separately.
    /// </summary>
    public static void Destroy()
    {
        var owner = (UInt160)Storage.Get(Storage.CurrentContext, OwnerKey);
        if (!Runtime.CheckWitness(owner)) throw new Exception("owner only");

        // Sweep any token balances held at this contract before destruction
        // (user must list which tokens to sweep — there's no implicit sweep).
        // Example for GAS:
        var gasBal = (BigInteger)Contract.Call(GAS.Hash, "balanceOf", CallFlags.ReadOnly,
                                               new object[] { Runtime.ExecutingScriptHash });
        if (gasBal > 0)
            Contract.Call(GAS.Hash, "transfer", CallFlags.All,
                          new object[] { Runtime.ExecutingScriptHash, owner, gasBal, "destroy-sweep" });

        OnDestroyed(Runtime.ExecutingScriptHash);
        ContractManagement.Destroy();
    }
}
```

### What's Different

| Aspect | EVM `selfdestruct` | Neo `ContractManagement.Destroy` |
| --- | --- | --- |
| Trigger | Opcode in middle of execution | Explicit method call |
| Auth | Implicit (whoever holds caller authority) | Explicit `Runtime.CheckWitness` check |
| Bytecode removal | Conditional (post-6780) | Always |
| Fund handling | Implicit ETH transfer | Application code transfers out before destroy |
| Gas refund | Was a thing pre-EIP-3529 | Never existed |
| Same-tx redeploy | Possible via CREATE2 | Not applicable — script hash is content-derived |

</template>

</StandardEntry>

</StandardsMirror>