在过去一年 Web3 安全事件里,有一种漏洞特别可怕:项目没有被黑代码漏洞,而是整个合约被黑客“接管”了。这种问题通常出现在 代理合约升级机制里,而罪魁祸首往往是一行 delegatecall或权限控制写错。
今天我们拆解一个常见漏洞:代理合约初始化漏洞。
很多 DeFi 项目在
Ethereum
上部署合约时,不会把所有逻辑写在一个合约里,而是使用代理模式。
原因很简单:
区块链合约一旦部署,代码就不能改。
如果项目需要升级,就必须使用代理结构。
典型结构是:
用户 → Proxy合约 → Logic合约
Proxy 负责存储数据,
Logic 负责执行代码。
Proxy 在执行逻辑时会调用:
delegatecall
delegatecall的作用非常特殊:
它会在当前合约的存储空间里执行另一个合约的代码。
也就是说:
代码来自 Logic 合约
数据来自 Proxy 合约
这也是升级代理能够工作的原因。
但如果设计不好,黑客就能利用这一点。
先看一个典型的 Proxy 初始化代码:
contract Proxy {
address public implementation;
address public admin;
function initialize(address _impl) public {
implementation = _impl;
admin = msg.sender;
}
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success);
}
}
逻辑很简单:
1️⃣ 设置逻辑合约地址
2️⃣ 设置管理员
3️⃣ 之后所有调用都 delegatecall 给 implementation
看起来没问题。
但这里有一个致命问题:
initialize 没有限制只能调用一次。
如果部署流程是:
1 项目部署 Proxy
2 打算稍后初始化
但此时 任何人都可以先调用 initialize。
攻击者只需要:
initialize(恶意逻辑合约地址)
然后自己就会成为:
admin = msg.sender
此时黑客就拥有整个合约系统的控制权。
攻击流程非常简单:
第一步:黑客调用
initialize(attackerImplementation)
第二步:部署恶意逻辑合约:
contract Evil {
function drain() public {
payable(msg.sender).transfer(address(this).balance);
}
}
第三步:通过 Proxy 调用恶意函数。
因为 Proxy 会执行:
implementation.delegatecall(msg.data)
于是恶意代码会在 Proxy 的存储环境里执行。
结果就是:
👉 合约里的资金直接被转走。
这种漏洞其实已经在 Web3 历史上多次出现,其中最经典的事故之一是:
Parity Wallet Hack
这个事件导致多个多签钱包被冻结或被接管,总损失超过 1 亿美元。
相关技术生态:
Solidity
OpenZeppelin
那么正确写法是什么?
第一件事:初始化函数只能执行一次。
标准写法:
bool private initialized;
function initialize(address _impl) public {
require(!initialized, "already initialized");
implementation = _impl;
admin = msg.sender;
initialized = true;
}
第二种更安全的方式,是直接使用成熟的升级框架,比如:
OpenZeppelin Transparent Proxy
这些库会自动处理:
初始化锁
权限管理
升级逻辑
总结一下,用一句最通俗的话理解这个漏洞:
如果你的代理合约没锁初始化,黑客就能先把自己设置成管理员。
然后结果就是:
👉 整个协议归黑客所有。
在 Web3 安全圈有一句很经典的话:
“合约漏洞不一定偷钱,但权限漏洞一定能偷钱。”
ChainSafeAI(链熵科技)专注于区块链生态安全,以“数据驱动 + 技术赋能”构建360°全方位安全防护体系,服务于交易所、金融机构、OTC服务商及加密资产投资者。
公司提供覆盖KYT风险监测、智能合约审计、加密资产追踪、区块链漏洞测试等在内的全维度安全与合规技术解决方案,助力客户防范洗钱、诈骗等风险,保障业务合规运行。
通过实时风险预警、合规审查与资金溯源分析,协助客户识别链上异常行为、防范洗钱及诈骗风险、降低被盗损失并提升资产追回可能性。



被折叠的 条评论
为什么被折叠?



