在过去一年的 Web3 审计报告中,Delegatecall 存储碰撞(Storage Collision) 是出现频率非常高、但很多开发者仍然低估的漏洞类型。大量协议为了实现可升级架构或模块化系统,使用 delegatecall 执行外部逻辑代码,但如果存储布局设计不当,攻击者就可能通过一次调用直接篡改核心变量甚至接管协议。本文将从底层 EVM 机制、漏洞代码解析以及攻击流程进行一次专业级分析。
一、为什么 Delegatecall 危险
在 Ethereum Virtual Machine 中有四种常见调用方式:
call
delegatecall
staticcall
callcode (已废弃)
delegatecall 的关键特性是:
👉 代码来自目标合约,但存储来自当前合约
也就是说:
执行代码:外部合约
使用存储:当前合约
msg.sender:保持不变
这正是升级代理(Proxy Pattern)广泛使用 delegatecall 的原因。
但如果两个合约的 storage layout 不一致,就会产生灾难。
二、存储碰撞的本质
在 Solidity 中,变量按顺序存储:
slot 0
slot 1
slot 2
...
例如一个代理合约:
contract Proxy {
address public owner; // slot 0
address public implementation; // slot 1
}
而逻辑合约:
contract Logic {
uint256 public value; // slot 0
}
当 Proxy 执行:
implementation.delegatecall(data)
逻辑合约访问 slot0 时:
实际上访问的是 Proxy 的 slot0
也就是说:
Logic.value → Proxy.owner
这就是 Storage Collision。
三、漏洞示例代码
一个典型的代理合约:
contract VulnerableProxy {
address public owner;
address public implementation;
constructor(address impl) {
owner = msg.sender;
implementation = impl;
}
fallback() external payable {
(bool success, ) =
implementation.delegatecall(msg.data);
require(success);
}
}
逻辑合约:
contract LogicContract {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
}
乍看没有问题。
但如果攻击者部署一个 恶意逻辑合约。
四、攻击合约
攻击者逻辑:
contract AttackLogic {
address public attacker;
function setValue(uint256) public {
attacker = msg.sender;
}
}
存储布局:
slot0 → attacker
而 Proxy:
slot0 → owner
当执行:
setValue()
实际上发生的是:
attacker → Proxy.owner
攻击者直接成为 owner。
五、真实攻击流程
攻击流程通常如下:
1️⃣ 部署恶意逻辑合约
2️⃣ 诱导 Proxy 使用该实现
例如:
upgradeTo(maliciousImplementation)
3️⃣ 调用恶意函数
proxy.call(setValue())
4️⃣ 存储碰撞触发
Proxy.owner = attacker
5️⃣ 接管协议
六、为什么这个漏洞在 Web3 中如此常见
原因主要有三个。
1️⃣ 可升级架构非常普遍
大量项目使用 Proxy Pattern:
OpenZeppelin Proxy
Compound Labs Comptroller
Aave Upgradeable contracts
2️⃣ 开发者忽略 Storage Layout
很多开发者认为:
变量名不同 → 没问题
但 EVM 只看:
slot index
3️⃣ Solidity 升级带来变化
不同版本编译器:
storage packing
layout
都可能不同。
七、EVM 层面的解释
在 **Solidity 编译后,变量会映射到 storage slot。
例如:
owner -> slot0
implementation -> slot1
EVM 操作是:
SLOAD(slot)
SSTORE(slot)
delegatecall 不会改变 slot。
所以逻辑合约访问:
SSTORE(0)
其实修改的是 Proxy。
八、专业防御方案
安全架构通常采用三种方法。
1️⃣ 固定 Proxy 存储布局
使用 EIP-1967 标准:
implementation slot =
keccak256("eip1967.proxy.implementation") - 1
这样不会与普通变量冲突。
这个标准被很多框架采用,例如:
OpenZeppelin Upgradeable Contracts
2️⃣ 使用 Storage Gap
逻辑合约预留 storage:
uint256[50] private __gap;
避免未来变量冲突。
3️⃣ 严格升级权限控制
升级函数必须限制:
require(msg.sender == owner);
并加入:
Timelock
Multisig
例如:
Gnosis Safe
九、审计人员如何检测
专业审计团队通常检查:
1️⃣ Proxy storage layout
2️⃣ Logic contract layout
3️⃣ delegatecall usage
典型检查点:
fallback delegatecall
upgrade function
storage slot mapping
很多审计工具也会自动检测,例如:
Slither
Mythril
十、总结
Delegatecall 存储碰撞漏洞的本质其实很简单:
delegatecall
+ storage layout mismatch
= critical privilege takeover
攻击者可以:
修改 owner
修改 admin
修改资金地址
这类漏洞往往:
🚨 一旦存在就是协议级灾难
也是 Web3 审计中 严重级别(Critical) 的问题。
ChainSafeAI(链熵科技)专注于区块链生态安全,以“数据驱动 + 技术赋能”构建360°全方位安全防护体系,服务于交易所、金融机构、OTC服务商及加密资产投资者。
公司提供覆盖KYT风险监测、智能合约审计、加密资产追踪、区块链漏洞测试等在内的全维度安全与合规技术解决方案,助力客户防范洗钱、诈骗等风险,保障业务合规运行。
通过实时风险预警、合规审查与资金溯源分析,协助客户识别链上异常行为、防范洗钱及诈骗风险、降低被盗损失并提升资产追回可能性。
823



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



