返回论坛

EVM 深度解析:CALL与DELEGATECALL操作码的技术剖析

查找币 学术研究 安全研究 Web3安全 区块链安全

查找币安全研究院

钱包恢复评估 | 链上取证分析 | Web3 事件响应
以合法授权、证据保全、隐私保护和可复核流程为前提,不要求用户在线提交完整私钥或助记词。

查看研究院 研究报告中心
在智能合约开发与安全审计过程中,理解EVM底层执行机制至关重要。查找币安全团队在近期审计中发现,许多合约漏洞的根源在于开发者对CALL与DELEGATECALL操作码的理解不够深入。本文将基于EVM执行上下文,结合Solidity示例和Geth客户端实现,系统分析这两个核心操作码的工作原理。 ## 合约执行上下文 EVM在执行智能合约时,会为每个调用创建一个独立的执行上下文,包含以下关键组件: ### 代码区(Code) - 合约字节码存储在链上,通过合约地址引用,不可修改 - EOA账户代码区为空 - 可通过`CODESIZE`、`CODECOPY`读取自身代码 - 其他合约可通过`EXTCODESIZE`、`EXTCODECOPY`读取 ### 栈(Stack) - 每个调用上下文初始化空栈,由32字节元素组成 - 存储指令的输入输出,后进先出 - 最大限制:1024个值 - 支持`PUSH`、`POP`、`DUP`、`SWAP`等指令操作 ### 内存(Memory) - 每个调用上下文初始化空内存,初始值为0 - 非持久化,调用结束后销毁 - 主要通过`MLOAD`、`MSTORE`读写 - 也可通过`CREATE`、`EXTCODECOPY`等指令访问 ### 存储区(Storage) - 持久化存储,跨执行保留 - 32字节插槽到32字节值的映射 - 每个合约独立存储,不可互访 - 通过`SLOAD`、`SSTORE`读写 - 未写入的键返回0 ### 调用数据(Calldata) - 交易传入的数据,不可修改 - 合约创建时,calldata为构造器代码 - 内部调用(xCALL)会创建新calldata区域 - 通过`CALLDATALOAD`、`CALLDATASIZE`、`CALLDATACOPY`读取 ### 返回数据(Return Data) - 合约调用后的返回值 - 由`RETURN`、`REVERT`指令设置 - 通过`RETURNDATASIZE`、`RETURNDATACOPY`读取 ## Solidity实例分析 让我们通过一个具体案例理解CALL与DELEGATECALL的区别。 ### 合约部署配置 - EOA地址:`0x5B38Da6a701c568545dCfcB03FcB875f56beddC4` - 合约A地址:`0x7b96aF9Bd211cBf6BA5b0dd53aa61Dc5806b6AcE` - 合约B地址:`0x3328358128832A260C76A4141e19E2A943CD4B6D` ### 调用参数 - 合约B地址作为目标 - uint值:12 - 转账金额:1 ETH ### 关键差异 **DELEGATECALL执行流程:** 1. 上下文保留:msg.sender = EOA,msg.value = 1 ETH 2. 存储操作:SLOAD读写合约A的存储区 3. 代码执行:执行合约B的字节码 4. 状态变更:修改合约A的存储 **CALL执行流程:** 1. 上下文切换:msg.sender = 合约A,msg.value = 1 ETH 2. 存储操作:SLOAD读写合约B的存储区 3. 代码执行:执行合约B的字节码 4. 状态变更:修改合约B的存储 ## Geth客户端实现深度解析 ### 核心数据结构 在`core/vm/evm.go`中,EVM结构体定义了执行环境: ```go type EVM struct { Context BlockContext TxContext TxContext StateDB StateDB depth int chainConfig *params.ChainConfig chainRules params.Rules interpreters []Interpreter interpreter Interpreter abort int32 callGasTemp uint64 returnData []byte } ``` ### 执行上下文组件 **TxContext**定义交易级上下文: ```go type TxContext struct { gasPrice *big.Int origin common.Address coinbase common.Address nonce uint64 gasLimit uint64 blockNumber *big.Int time *big.Int difficulty *big.Int } ``` **ContractRef**接口定义了合约引用: ```go type ContractRef interface { Address() common.Address } ``` **AccountRef**实现了该接口: ```go type AccountRef common.Address func (a AccountRef) Address() common.Address { return common.Address(a) } ``` ### CALL操作码实现 在`core/vm/instructions.go`中,CALL操作码的关键逻辑: ```go func opCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { stack := scope.Stack // 从栈中获取参数 gas := stack.Back(0) addr := stack.Back(1) value := stack.Back(2) inOffset := stack.Back(3) inSize := stack.Back(4) retOffset := stack.Back(5) retSize := stack.Back(6) // 创建新上下文 contract := NewContract(caller, AccountRef(addr), value, gas) contract.SetCallCode(&addr, code) // 执行子调用 ret, err := interpreter.evm.Call(contract, input, gas) } ``` ### DELEGATECALL操作码实现 ```go func opDelegateCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { stack := scope.Stack gas := stack.Back(0) addr := stack.Back(1) inOffset := stack.Back(2) inSize := stack.Back(3) retOffset := stack.Back(4) retSize := stack.Back(5) // 关键:保持调用者上下文 contract := NewContract(caller, caller.Address(), value, gas) contract.SetCallCode(&addr, code) contract.DelegateCall = true // 执行委托调用 ret, err := interpreter.evm.DelegateCall(contract, input, gas) } ``` ### 核心差异分析 1. **地址传递**: - CALL:`AccountRef(addr)`,目标合约地址 - DELEGATECALL:`caller.Address()`,原始调用者地址 2. **存储访问**: - CALL:SLOAD/SSTORE操作目标合约存储区 - DELEGATECALL:SLOAD/SSTORE操作原始调用者存储区 3. **上下文保留**: - CALL:创建全新上下文,msg.sender变为调用者 - DELEGATECALL:保留原始msg.sender和msg.value ## 安全审计视角 查找币安全团队在审计中发现,DELEGATECALL的误用是常见漏洞来源: 1. **存储布局冲突**:当代理合约和目标合约存储布局不一致时,可能导致存储覆盖 2. **权限绕过**:DELEGATECALL保留msg.sender,可能绕过权限检查 3. **重入攻击**:不当使用可能导致重入漏洞 ### 最佳实践建议 1. 使用DELEGATECALL时确保存储布局一致 2. 实现访问控制时考虑msg.sender的上下文 3. 使用OpenZeppelin的Proxy模式作为参考实现 4. 在审计中重点关注DELEGATECALL的调用链 ## 总结 通过本次EVM深度解析,我们明确了CALL与DELEGATECALL在存储、上下文、地址传递方面的本质差异。理解这些底层机制对于编写安全的智能合约至关重要。下一期我们将深入探讨EVM交易收据和事件日志的数据结构。 --- *本文由查找币安全团队整理发布*
在论坛中查看和回复