# selfdestruct详解

By [xyyme.eth](https://paragraph.com/@xyyme) · 2022-03-30

---

Solidity 中的 selfdestruct 函数是一个内置函数，需要接收一个参数。我们先来看看文档是怎么描述 selfdestruct 的：

> The only way to remove code from the blockchain is when a contract at that address performs the `selfdestruct` operation. The remaining Ether stored at that address is sent to a designated target and then the storage and code is removed from the state. Removing the contract in theory sounds like a good idea, but it is potentially dangerous, as if someone sends Ether to removed contracts, the Ether is forever lost.

selfdestruct 可以清除合约的代码，且是唯一清除代码的方法。当调用 selfdestruct 时，合约的所有 ETH 余额会发送给 selfdestruct 的参数地址，然后合约的代码和内存都被移除。销毁之后，如果给这个合约地址发送 ETH，那么就永久销毁了。

### 代码实践

我们来写段代码测试一下：

    pragma solidity 0.8.13;
    
    contract TestDestruct {
        uint public age;
        string public name;
        // 构造标记为payable，我们直接在部署的时候就打入ETH
        constructor(uint _age, string memory _name) payable {
            age = _age;
            name = _name;
        }
    
        function destruct() external {
            selfdestruct(payable(msg.sender));
        }
    
        function balance() external view returns (uint) {
            return address(this).balance;
        }
    }
    

部署合约，参数为（88，Tom），同时传入 1 ETH，查看状态变量与合约余额分别为：

*   age: 88
    
*   name: Tom
    
*   balance: 1 ETH
    

没有问题。同时注意到，Etherscan中的合约界面为（没有上传代码）：

![](https://storage.googleapis.com/papyrus_images/1c8033c0726431109a068f99f9825be5648718d5261031113577773ea846d2e0.png)

那么我们再调用一下 destruct 函数，此时再查看状态变量与合约余额分别为：

*   age: 0
    
*   name: （为空）
    
*   balance: 0
    

同时再看 Etherscan 的这笔交易：

![](https://storage.googleapis.com/papyrus_images/015ca065d9688145348ed587416a42ad0d6c2e4ba42d47c51cf2675b9bf83d82.png)

很明显能看出，销毁时，合约余额转给了 selfdestruct 的参数地址，也就是 `msg.sender`。

此时，我们再看 Etherscan 的合约页面：

![](https://storage.googleapis.com/papyrus_images/f91f4b1eaff863144fce16279cef833a943311755618e1d3cd14931cd9109f94.png)

可以看到合约代码已经被销毁了。

其实，当合约调用 selfdestruct 被销毁时，由于合约本身的代码和数据都已经销毁，因此合约地址此时就已经变成了一个 EOA 地址。那么如果向这个地址发送 ETH，除非能够找到这个地址所对应的私钥，那么这些 ETH 便是永久销毁了。但是这里有一个特例，就是与 create2 相结合，就可以碰撞出神奇的火花。

### 与 create2 结合

之前的[文章](https://mirror.xyz/xyyme.eth/czipkrvqRwxHUjQey7zeEScfWDEVUYNajMAEo5e7Myw)我们介绍过 create2 操作码，对于同一个合约，如果参数与盐都相同，那么工厂合约部署的时候，得到的一定是相同的地址。但是由于第一次已经部署了，所以第二次部署是会失败的。但是如果第一次部署的合约中可以 selfdestruct，那么按照我们前面的结论，selfdestruct 之后这个地址已经变成 EOA 地址了，那么能不能再在相同地址上部署一下呢。来试试：

    pragma solidity 0.8.13;
    
    import "@openzeppelin/contracts/utils/Create2.sol";
    
    contract Create2WithDestruct {
        event Deployed(address addr);
        function deploy() public {
            // 部署合约，参数传88
            address addr = Create2.deploy(
                0,
                keccak256("Here is salt"),
                abi.encodePacked(type(DemoOne).creationCode, abi.encode(88))
            );
            // 记录地址
            emit Deployed(addr);
        }
    }
    
    contract DemoOne {
        uint public age;
        constructor(uint _age) {
            age = _age;
        }
    
        function destruct() external {
            selfdestruct(payable(msg.sender));
        }
    }
    

这段代码中，我们在 `Create2WithDestruct` 合约中，使用 create2 操作码部署 `DemoOne` 合约，参数传 88。部署后，事件记录为：

![](https://storage.googleapis.com/papyrus_images/e964395fb809dfdaee71eb3efb63d71edd32394e42a098e8ef4c7a28cabcb047.png)

即通过 create2 部署得到的 `DemoOne` 地址为：

> 0x10CEBC6a50B65320578bBfA6b96503c75f745424

通过这个地址来读取合约数据：

![](https://storage.googleapis.com/papyrus_images/ec72dbefc4fc3a5435d86d4d83a22c56d5333194a4090e43ba47a4bf77f5992e.png)

数据正确。我们尝试通过 create2 再次部署 `DemoOne` 合约：

![](https://storage.googleapis.com/papyrus_images/ebfb14c826813c991ea984d367145ba08471d33e6e0f305e24b5a9ab6bf983ed.png)

交易失败，这是因为这个地址上已经有数据了，因此不能再次部署，这与我们的预期一致。

现在我们尝试调用 `DemoOne` 合约的 selfdestruct 函数。调用成功，同时，age已经变成0：

![](https://storage.googleapis.com/papyrus_images/b13c697ff205931954dfea2dcc527000386315c01b33543100ca0443b62d9902.png)

按照我们之前的结论，此时之前的合约地址已经变成了 EOA，而在任何一个 EOA 地址上都是可以部署合约的，那么我们再来试试调用 deploy 部署一次合约：

![](https://storage.googleapis.com/papyrus_images/915e6c317b929865420def0ca1892c6928eb63b5f40a7f429283557c714dd4cd.png)

部署成功，同时得到的地址为：

> 0x10CEBC6a50B65320578bBfA6b96503c75f745424

这与我们之前第一次部署的时候得到的地址相同。也就是说，我们可以通过 selfdestruct 与 create2 相结合在同一个地址上多次部署合约。这样，我们前面说的当合约销毁后，向合约发送 ETH 将被永久销毁的说法在这里就不准确了。如果合约是由 create2 部署的，那么在合约被销毁之后，仍然可以通过再次部署合约得到这个地址的控制权。

### selfdestruct 发送 ETH 的神奇用法

前面我们说到，selfdestruct 中的参数地址会接收到合约的余额，而这个行为是强制性的，也就是说，即使这个参数地址是个合约，并且合约中通过 `receive` 或者 `fallback` 限制了接收 ETH，而 selfdestruct 中的发送行为仍然是可以发送给这个地址的。

来看段代码：

    pragma solidity 0.8.13;
    
    contract DestructTransfer {
        constructor() payable {
        }
    
        function destruct(address recipient) external {
            selfdestruct(payable(recipient));
        }
    }
    
    contract DemoOne {
        constructor() {
        }
    
        fallback() external payable {
            revert("You can not send ether");
        }
    
        receive() external payable {
            revert("You can not send ether");
        }
    
        function balance() external view returns (uint) {
            return address(this).balance;
        }
    }
    

`DemoOne` 合约通过 `fallback` 和 `receive` 的限制禁止接收 ETH。我们部署并转一点 ETH 试试：

![](https://storage.googleapis.com/papyrus_images/7c15a6a68a134a80f496676a7dbaa359755654004ae43934b902ec13e0b6df32.png)

交易失败，报错原因也是我们设定的信息。现在我们通过 `DestructTransfer` 来强行转入 ETH。部署合约的同时打入 1 ETH，并且调用 `destruct` 函数，参数为 `DemoOne` 的地址，交易成功后查看 `DemoOne` 的余额：

![](https://storage.googleapis.com/papyrus_images/5f4653c4bad213fdb5c01ad4f438183a9e841e383c53b96a04d74368a6809c8e.png)

可以看到即使合约中有接收限制，我们仍然将 ETH 强行打入了合约中。

实际上，一共有两种方法来强行向一个地址转入 ETH：

1.  ETH 挖矿时区块头中的 coinbase 字段设置为接收地址
    
2.  selfdestruct 的参数地址设置为接收地址
    

关于强行转入 ETH 这个话题，还有一些黑客游戏就是利用这个原理，比如这道 Ethernaut 中的[题目](https://ethernaut.openzeppelin.com/level/0x22699e6AdD7159C3C385bf4d7e1C647ddB3a99ea)。

### 总结

selfdestruct 是可以销毁合约的代码和数据，接收一个参数，销毁时会将合约中剩余的 ETH 全部打入改地址，并且是强制性的。selfdestruct 与 create2 结合可以实现在同一个地址上多次部署合约。

### 参考

[https://docs.soliditylang.org/en/latest/introduction-to-smart-contracts.html#deactivate-and-self-destruct](https://docs.soliditylang.org/en/latest/introduction-to-smart-contracts.html#deactivate-and-self-destruct)

[

What happens after selfdestruct is called?
------------------------------------------

https://ropsten.etherscan.io/address/0xf825d3b3d06a4b46379a3c276df7f26abd055463 After selfdestruct call was made, all the balance was transferred to the deployer of the contract. Why are withdraw

https://ethereum.stackexchange.com

![](https://storage.googleapis.com/papyrus_images/dc7b2a51adecb9025c3c259ca1207f5f2fc8ebc314cfd83fd79e6d0cba01a644.png)

](https://ethereum.stackexchange.com/questions/46813/what-happens-after-selfdestruct-is-called)

---

*Originally published on [xyyme.eth](https://paragraph.com/@xyyme/selfdestruct)*
