


























最残酷的安全漏洞不是偷走你的钱,而是把钱锁在你永远打不开的保险柜里,让你每天看着它,却无能为力。
当我第一次接触 selfdestruct 时,我以为它意味着:
✅ ~~合约从区块链上彻底删除~~
✅ ~~所有资金自动退还给用户~~
✅ ~~合约地址变成空地址~~
全错。
contract SelfDestructDemo {
uint256 public value = 100;
address public owner;
constructor() {
owner = msg.sender;
}
function kill() external {
require(msg.sender == owner);
selfdestruct(payable(owner)); // 💀 自毁
}
}
// 自毁前:
value = 100, 合约有 10 ETH
// 调用 kill() 后:
// 1. 10 ETH → 转给 owner ✅
// 2. value = 0 (存储清空) ✅
// 3. 字节码:仍然在链上! ❌ 不是预期
// 4. 再次调用 value():返回 0,不是报错! ❌ 不是预期
// 5. 再次调用 kill():交易成功,无事发生! ❌ 不是预期
自毁的精确后果:
关键认知:自毁后的合约没有消失,它变成了一个“植物人”——有心跳(可调用),没意识(无效果)。
// UUPS 架构:升级函数在逻辑合约中
contract UUPSLogic is UUPSUpgradeable {
address public owner;
mapping(address => uint256) public balances; // 假设锁了 1000 万 USDT
function withdraw() external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
// 转账逻辑...
}
// ❌ 危险函数:看起来无害,实则致命
function emergencyStop() external onlyOwner {
selfdestruct(payable(owner)); // 💀 自毁!
}
// 升级函数在逻辑合约中!
function _authorizeUpgrade(address) internal override onlyOwner {}
}
// 攻击或误操作后:
// 1. 逻辑合约自毁 → 代码标记为“已销毁”
// 2. 代理合约仍然指向这个“植物人”逻辑合约
// 3. 用户调用 withdraw() → ✅ 交易成功,但钱纹丝不动
// 4. 管理员调用 upgradeTo() → ✅ 交易成功,但 implementation 槽没变!
// 5. 💀 永久瘫痪,资产永久锁定
为什么无法升级?
// UUPS 升级需要 delegatecall 到逻辑合约
proxy.upgradeTo(newImpl);
// → proxy delegatecall 到 LogicV1
// → LogicV1 已自毁,代码是空操作
// → _setImplementation(newImpl) 永远不会执行
// → implementation 槽永远指向死掉的 LogicV1
// → 死循环,永远困住!
// 透明代理架构:升级函数在代理合约中
contract TransparentProxy {
address implementation; // 指向逻辑合约
address admin;
// 🛡️ 升级函数在代理中,不依赖逻辑合约!
function upgradeTo(address newImpl) external onlyAdmin {
implementation = newImpl;
}
fallback() external payable {
if (msg.sender == admin) {
// 管理函数在代理中执行
} else {
_delegate(implementation); // 业务转发
}
}
}
// 即使逻辑合约自毁:
// 1. 代理合约依然健在
// 2. admin 调用 upgradeTo → ✅ 成功修改 implementation
// 3. 指向新逻辑合约 → ✅ 业务恢复
// 4. 资产安全,服务恢复!
// 后果:短暂瘫痪,可恢复
// 假设这是一个锁定了 1000 万 USDT 的 UUPS 合约
// 自毁发生后:
// 1. 资金还在代理合约里
const balance = await USDT.balanceOf(proxy.address);
console.log(balance); // 10,000,000 USDT ✅ 钱还在!
// 2. 但所有业务函数都失效了
await proxy.withdraw();
// ✅ 交易成功!
// ❌ USDT 余额纹丝不动!
// ❌ 状态变量没变化!
// ❌ 什么都没发生!
// 3. 无法升级
await proxy.upgradeTo(newImpl);
// ✅ 交易成功!
// ❌ implementation 槽没变!
// ❌ 依然指向死掉的逻辑合约!
// 4. 无法直接转出
await USDT.transfer(owner, balance);
// 需要从代理合约调用,但代理合约的业务逻辑死了
// owner 根本无法操作代理合约里的 USDT!
// 5. 无法在 EVM 层面强制转出
// ERC20 标准没有提供“强制提取”功能
// 除非有私钥,否则无解
这就是资产锁定:钱在眼前,永远取不出。
时间:2017年11月
损失:约15亿美元 ETH(当时价值)
结局:至今仍锁在合约里,8年无法取出
技术真相:
// Parity 钱包库合约(简化)
contract WalletLibrary {
// ❌ 致命错误:public 函数,任何人都能调用!
function kill(address _to) public {
selfdestruct(_to); // 自毁
}
}
// 攻击者调用 kill()
// 库合约自毁 → 所有依赖这个库的钱包合约失去业务逻辑
// 每个钱包合约里锁着数万到数百万 ETH
// 资金还在,但永远取不出来
这不是“被盗”,这是“被判了无期徒刑”。
// 场景 A:资产被盗
// 用户视角:虽然难受,但还有希望
"我的钱被黑客转走了,FBI在追,交易所冻结了地址..."
// 场景 B:资产锁定(UUPS自毁)
// 用户视角:每天看着余额,无能为力
"钱还在合约里,我随时能查到余额,但就是取不出来...
项目方说无法升级,无法恢复,只能等奇迹...
一年过去了,两年过去了,余额还是那个数字..."
哪个更残忍?
// 2015-2017 年的“最佳实践”
contract MyContract {
function kill() external onlyOwner {
selfdestruct(payable(owner)); // 当时认为这是“优雅退出”
}
}
// 传统开发者的直觉
contract OldSystem {
function delete() external { // 传统数据库可以删除
this.cleanup();
this.destroy();
}
}
// 移植到区块链的错误类比
contract NewContract {
function migrate() external onlyOwner {
selfdestruct(payable(newVersion)); // ❌ 这不是迁移,是自杀!
}
}
// 开发者以为:
"我只是销毁逻辑合约,代理合约还能升级吧?"
// 残酷真相:
"UUPS 的升级函数在逻辑合约里,逻辑合约死了,升级这条路就永远堵死了。"
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract SafeContract is UUPSUpgradeable, OwnableUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // 法则1:禁止逻辑合约初始化
}
function initialize() initializer public {
__Ownable_init();
__UUPSUpgradeable_init();
}
// 法则2:必须重写升级授权函数
function _authorizeUpgrade(address) internal override onlyOwner {}
// ❌ 法则3:绝不写 selfdestruct
// function kill() external onlyOwner { selfdestruct(...); } // 禁止!
// ❌ 法则4:绝不写不可信的 delegatecall
// function execute(address target) external { target.delegatecall(...); } // 禁止!
// ✅ 法则5:用 pause 替代自毁
bool public isEmergency;
function pause() external onlyOwner {
isEmergency = true;
}
function unpause() external onlyOwner {
isEmergency = false;
}
modifier whenNotPaused() {
require(!isEmergency, "Paused");
_;
}
}
// 以下任何一行出现在 UUPS 逻辑合约中 = 一票否决
selfdestruct(payable(...));
suicide(...); // 旧版本别名
assembly { selfdestruct(...) }
delegatecall(gas(), target, ...) // 如果 target 可控
// 立即升级到新的逻辑合约
await proxy.connect(admin).upgradeTo(newImplAddress);
// 业务恢复,资产安全
// 残酷的答案:目前没有任何可靠方案
// 1. 不能升级(upgradeTo 已死)
// 2. 不能直接转出 ERC20(没有私钥操作)
// 3. 不能硬分叉(除非整个社区为你改规则)
// 唯一理论上的可能(几乎不可行):
// - 找到逻辑合约自毁前的 storage 证明
// - 通过复杂的 Merkle 证明逐个提取
// - 成本极高,技术极难,从未成功过
接受现实,当作昂贵的安全课。
自毁不会让合约消失,只会让它变成植物人。
透明代理的自毁是感冒,UUPS 的自毁是癌症。
资产锁定比资产被盗更痛苦——被盗还有希望追回,锁定是永恒的绝望。
这是区块链世界最残酷的 Bug 之一:
它不是让黑客暴富,而是让普通用户每天看着自己的余额,却永远取不出来。
记住:在 UUPS 合约中,selfdestruct 不是退出机制,是集体陪葬。
本文基于以太坊 EVM 架构(2026年),Solidity 0.8.x,OpenZeppelin 合约库。安全建议请以官方最新文档为准。
如果你从这篇文章只记住一句话,请记住:
在 UUPS 逻辑合约里写 selfdestruct,等于亲手给用户的资产判了无期徒刑。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。