欢迎光临
我们一直在努力

WTF-Solidity 合约删除指南:selfdestruct 从基础用法到 EIP-6780 语义变迁

WTF-Solidity 合约删除指南:selfdestruct 从基础用法到 EIP-6780 语义变迁

【免费下载链接】WTF-Solidity WTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy 【免费下载链接】WTF-Solidity 项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

selfdestruct(自毁)是 Solidity 中唯一能真正“删除”智能合约的操作码,它同时会把合约内剩余的 ETH 强制转入指定地址,常被视为智能合约的“紧急停止按钮”。本文以 WTF-Solidity 教程第 26 讲的日文版文档 Languages/ja/26_DeleteContract_ja/readme.md 为骨架,结合仓库中 DeleteContract.sol 与 DeployContract.sol 两个可运行示例,系统讲解 selfdestruct 的语法、Cancun(坎昆)升级前后行为差异,以及编写安全的自毁逻辑时需要注意的陷阱。读完本文,你将掌握 selfdestruct 的完整用法,理解 EIP-6049 与 EIP-6780 对其语义的影响,并能亲手在 Remix 中复现“删除合约”与“同笔交易内创建-自毁”两种验证流程。

selfdestruct 是什么

selfdestruct 命令用于删除智能合约,并将该合约中剩余的 ETH 转移到指定地址。它最初被设计用来应对合约出现无法挽回的错误的极端情况,相当于合约的“紧急制动”:

selfdestruct(_addr);

其中 _addr 是接收合约剩余 ETH 的目标地址。一个容易被忽略的细节是:_addr 地址不需要拥有 receive() 或 fallback() 函数也能接收 ETH——selfdestruct 的转账是强制性的,不存在调用目标合约代码的逻辑,因此不会像普通转账那样因目标合约拒绝接收而失败。

命名历史:从 suicide 到 selfdestruct

该操作码最早被命名为 suicide(自杀),但由于这个词过于敏感,为了保护抑郁的程序员群体,以太坊社区将其改名为 selfdestruct。在 Solidity v0.8.18 版本中,selfdestruct 关键字被标记为「不再建议使用」(deprecated):它会在某些情况下导致预期之外的合约语义,但由于目前还没有替代方案,Solidity 编译器仅对开发者输出编译阶段警告,相关内容可参见 EIP-6049。

Cancun 升级与 EIP-6780:语义发生了根本性变化

在以太坊坎昆(Cancun)升级中,EIP-6780 被纳入,其初衷是为了对 Verkle Tree 提供更好的支持。EIP-6780 缩减了 SELFDESTRUCT 操作码的功能范围,导致合约删除行为的语义发生根本性变化:

  • 已部署的合约无法再被 SELFDESTRUCT 删除:EIP-6780 之后,SELFDESTRUCT 唯一保留的能力是把合约中的 ETH 转移到指定地址,原本的“删除合约代码与存储”功能被大幅收窄。
  • 若要使用原先的删除功能,必须在同一笔交易中创建并自毁合约:只有“合约创建-自毁”这两个操作发生在同一笔交易时,删除效果才会生效。
  • 换言之,升级后 SELFDESTRUCT 从一个“删除工具”退化成了一个“强制转账工具”;想要复现旧版的完整删除语义,只能借助另一个合约在单笔交易内完成 new 创建与 selfdestruct 调用。

    Demo 1:DeleteContract——转移 ETH 与(升级前)自毁

    仓库中的 DeleteContract.sol(中文版位于 26_DeleteContract/DeleteContract.sol)完整演示了这一用法,合约声明 pragma solidity ^0.8.34;,与仓库根目录 foundry.toml 中统一指定的 solc = "0.8.34" 编译器版本一致:

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.34;

    // selfdestruct: 删除合约,并强制将合约剩余的ETH转入指定账户

    contract DeleteContract {

    uint public value = 10;

    constructor() payable {}

    receive() external payable {}

    function deleteContract() external {
    // 调用selfdestruct销毁合约,并把剩余的ETH转给msg.sender
    selfdestruct(payable(msg.sender));
    }

    function getBalance() external view returns(uint balance){
    balance = address(this).balance;
    }
    }

    合约包含三个关键部分:

    • uint public value = 10;:公开状态变量,用于验证自毁后存储是否被清除。
    • constructor() payable {} 与 receive() external payable {}:分别让合约在部署时和部署后都能接收 ETH。
    • deleteContract():调用 selfdestruct(payable(msg.sender)),把合约内所有 ETH 转给调用者。
    • getBalance():返回 address(this).balance,用于观察合约当前 ETH 余额。

    操作流程与预期结果

  • 部署合约,并向 DeleteContract 转入 1 ETH。此时调用 getBalance() 返回 1 ETH,value 为 10。
  • 调用 deleteContract() 触发 selfdestruct:
    • 坎昆升级前:合约会被彻底自毁,1 ETH 转回调用者地址,之后再调用合约函数交互会失败。
    • 坎昆升级后:合约依然存在、依然可以被调用,只是其内部 ETH 余额被转移到指定地址(归零),value 等存储也不会被清除。
  • Remix 验证:升级前与升级后的差异

    坎昆升级之前
  • 部署合约并转入 1 ETH,查看合约状态:

    坎昆升级前部署 DeleteContract 并转入 1 ETH 后的合约状态

  • 调用 deleteContract() 销毁合约,再次查看合约状态:

    坎昆升级前调用 selfdestruct 后合约状态

  • 测试中观察合约状态可以发现:销毁后 ETH 已返还给指定地址,且再次调用合约函数进行交互会失败——这正是旧版 SELFDESTRUCT 的完整删除语义。

    坎昆升级之后
  • 部署合约并转入 1 ETH,查看合约状态:

    坎昆升级后部署 DeleteContract 并转入 1 ETH 后的合约状态

  • 调用 deleteContract() 销毁合约,再次查看合约状态:

    坎昆升级后调用 selfdestruct 后合约状态

  • 升级后的测试结果截然不同:合约包含的 ETH 已经清零(返还给指定地址),但再次调用合约函数交互依然可以成功。这正是 EIP-6780 生效的直接体现。

    Demo 2:同笔交易内实现“合约创建-自毁”

    根据 EIP-6780 提案,旧版的删除功能只有在「合约创建-自毁」这两个操作处于同一笔交易时才能生效,因此需要借助另一个合约进行控制。仓库中的 DeployContract.sol(中文版位于 26_DeleteContract/DeployContract.sol)演示了这种做法:

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.34;

    import "./DeleteContract.sol";

    contract DeployContract {

    struct DemoResult {
    address addr;
    uint balance;
    uint value;
    }

    constructor() payable {}

    function getBalance() external view returns(uint balance){
    balance = address(this).balance;
    }

    function demo() public payable returns (DemoResult memory){
    DeleteContract del = new DeleteContract{value:msg.value}();
    DemoResult memory res = DemoResult({
    addr: address(del),
    balance: del.getBalance(),
    value: del.value()
    });
    del.deleteContract();
    return res;
    }
    }

    demo() 函数在同一笔交易内依次完成以下步骤:

  • new DeleteContract{value: msg.value}():用调用者转入的 ETH 作为初始余额,在交易中创建 DeleteContract 实例。
  • 记录创建后新合约的地址、余额(del.getBalance())与状态变量(del.value())到 DemoResult 结构体中。
  • 调用 del.deleteContract() 触发 selfdestruct。
  • 返回 res,将新合约的地址、自毁前的余额与状态一并暴露给调用者,便于链下验证。
  • 由于创建与自毁发生在同一笔交易内,即使坎昆升级之后,DeleteContract 也能被真正删除,其 ETH 会转移到创建它的 DeployContract。

    Remix 验证

  • 部署 DeployContract 合约,转入 1 ETH 并调用 demo() 方法,查看返回的合约状态,可确认 DeleteContract 已被正确部署,且 selfdestruct 后 ETH 已转移到 DeployContract:

    同笔交易 demo 调用后 DeployContract 的状态与返回结果

  • 将返回值中的 addr 地址作为 DeleteContract 导入 Remix,可以观察到该地址不存有 ETH,且调用其合约函数交互均失败:

    导入返回值中的 DeleteContract 地址后显示无 ETH 且调用失败

  • 注意事项:安全与信任问题

    在合约中引入 selfdestruct 功能时,需要格外谨慎:

  • 权限控制:对外提供合约销毁接口时,最好设置为只有合约所有者可以调用,可以使用函数修饰符 onlyOwner 进行声明(可参考本仓库第 11 讲 Owner.sol 中修饰符的写法),避免任意用户触发自毁。
  • 攻击向量:合约中的 selfdestruct 功能会为攻击者打开攻击向量。例如,攻击者可以利用 selfdestruct 向一个合约频繁转入代币发起攻击,由于自毁转账不需要经过目标合约的代码逻辑,这将大大节省攻击所需的 GAS 费用——虽然现实中很少有人这么做,但仍是潜在风险。
  • 信任问题:selfdestruct 功能的存在会降低用户对合约的信心。用户无法确定合约中的资产是否会在某一天被强制转走,这会影响合约的长期可信度。
  • 总结

    selfdestruct 是智能合约的“紧急按钮”:销毁合约并将剩余 ETH 转移到指定账户。当年著名的 The DAO 攻击发生时,以太坊的创始人们或许后悔过没有在合约中加入 selfdestruct 来停止黑客的攻击。而在坎昆升级(EIP-6780)之后,selfdestruct 的作用已经逐渐发生改变——从“删除合约”退化为“强制转账”,原本的删除能力只有在单笔交易内“创建-自毁”时才能保留。技术在不断演进,什么都不是一成不变的,持续学习才是应对变化的最好方式。

    【免费下载链接】WTF-Solidity WTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy 【免费下载链接】WTF-Solidity 项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

    创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

    赞(0)
    未经允许不得转载:171主机测评 » WTF-Solidity 合约删除指南:selfdestruct 从基础用法到 EIP-6780 语义变迁
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址