仅供教育参考,不构成投资建议;投资可能产生损失。
直接答案
delegatecall 在调用者合约的上下文中执行目标合约的代码。调用者保留自己的存储、余额和 address(this),而 msg.sender 和 msg.value 则保留原始调用中的值。
这种行为让代理合约、库和智能钱包模块成为可能,但也会把调用者的实际权限交给被委托执行的代码。存储写入、资产转移、授权或外部调用均以调用者而非提供代码的目标合约身份执行。
应将每个可触达的 delegatecall 目标都视为特权代码。安全性取决于目标选择规则、存储布局是否兼容、初始化状态、升级控制,以及该笔交易实际使用的确切实现。
工作原理
普通外部调用会读写被调用者自己的存储。使用 delegatecall 时,目标字节码则在调用者的存储上运行:一条 SSTORE 指令会更改属于调用者的存储槽。目标代码中的变量名在运行时并不重要,只有计算得到的存储槽位置才重要。
因此需要审计以下四个边界:
- 目标控制: 确定目标地址是固定的、由用户选择的、通过注册表解析的,还是可由管理员更改的。
- 存储兼容性: 对比每个实现版本的变量顺序、类型、继承关系、存储间隙,以及命名空间槽或标准化槽。
- 初始化与授权: 确认初始化器无法重复执行,并确认升级或模块管理函数会强制校验预期的调用者和治理延迟。
- 返回值处理: 验证失败会向上传播,且返回数据按预期类型解码;低级调用不会提供 Solidity 通常具备的合约类型检查。
ERC-1967 将实现、信标和管理员地址放在正常编译器分配范围之外的标准化存储槽中,从而减少代理合约的存储冲突。但它并不能证明某个实现是安全的,也不能证明经授权的升级是善意的。
示例
假设某个钱包将 owner 存储在 slot 0。一个插件在编译时把 counter 放在 slot 0,钱包通过 delegatecall 调用该插件时,插件递增这个计数器。
由于存储属于钱包,这次写入会改变钱包的 owner 值。如果写入后的字编码了攻击者控制的地址,后续授权检查就可能把攻击者识别为所有者,尽管插件本身从未持有钱包资产。
成功的交易回执无法区分预期状态变更和有害状态变更。因此,交易模拟应针对确切的代理地址和实现地址,检查存储差异、资产和授权变化、发出的事件以及下游调用。
风险
- 任意目标执行: 用户控制或验证薄弱的目标可以利用调用者的权限运行恶意代码。
- 存储冲突: 实现合约可以覆盖所有权、余额、暂停状态,甚至覆盖用于选择下一个实现的存储槽。
- 不安全升级: 管理员或治理流程遭到入侵后,可以在用户存入资产或授予授权之后替换此前已经审查的代码。
- 初始化失败: 未初始化的代理或实现合约可能允许其他账户取得特权角色,或配置危险的依赖项。
- 误导性检查: 只验证代理源码、当前实现或接口,可能会遗漏信标、待执行升级、模块注册表或其他执行路径。
签名前,应在近期区块上解析实现地址,核实谁能在何种延迟下更改它,检查目标的已验证字节码和存储布局,模拟完整调用数据,并比较执行前后的敏感存储与代币授权。对于智能钱包,还应审查模块如何启用、禁用,以及它们如何获准选择目标。
常见误区
- “目标无法动用调用者的资产。” 被委托执行的代码以调用者身份运行;只要调用者具备相应能力,它就能调用外部合约、转移资产或创建授权。
- “变量名相同就能避免冲突。” EVM 使用存储槽,而不是源代码中的名称。布局顺序、继承关系和类型必须保持兼容。
- “代理代码经过验证,就代表整个系统已经过验证。” 当前实现、信标、升级管理员、初始化状态和模块权限都是信任边界中彼此独立的部分。
相关主题
来源
- Introduction to Smart Contracts - Solidity Documentation (查阅日期: 2026-08-20)
- Units and Globally Available Variables - Solidity Documentation (查阅日期: 2026-08-20)
- ERC-1967: Proxy Storage Slots - Ethereum Improvement Proposals (查阅日期: 2026-08-20)