文章目录
-
- 外部调用函数
-
- call
- delegatecall
- call 与 delegatecall 的区别示例
-
- 部署后初始状态
- 调用B.testCall()函数
- 调用B.testDelegatecall()函数
- 区别总结
- 漏洞代码
- 代码审计
- 攻击代码
- 攻击原理解析
- 攻击流程
- 修复建议
- 审计思路
外部调用函数
在 Solidity 中,常见的两种底层外部函数调用方式是:call 和 delegatecall。
说明:callcode 已在 Solidity 0.5.0 版本后弃用,不再推荐使用,因此本文不再介绍。
call
call 是一种底层调用方法,适用于向其他合约发送消息或调用函数,同时也支持附带以太币(Ether)。它语法灵活,常用于动态调用。
(bool success, bytes memory data) = target.call{value: msg.value}(abi.encodeWithSignature("func(uint256)", 123));
特点:
1.使用的是目标合约的代码和存储环境;
2.当前合约的 msg.sender 和 msg.value 不会传递给被调用合约;
3.返回值为 (bool success, bytes memory data),失败时不会自动回退(revert),需手动处理;
4.常用于与未知接口或外部插件合约交互(如代理合约);
5.可发送 Ether,适用于多种场景。
delegatecall
delegatecall 也是底层调用方式,但与 call 不同,其执行上下文是当前合约的环境。
(bool success, bytes memory data) = target.delegatecall(abi.encodeWithSignature("func(uint256)", 123));
特点:
1.调用目标合约的代码,但使用的是当前合约的存储(storage)、msg.sender 和 msg.value;
2.目标合约不应定义自己的状态变量,否则可能引发存储冲突;
3.常用于代理合约(Proxy Pattern)与可升级合约场景;
4.存在安全风险,需谨慎使用被调用合约,防止覆盖关键变量。
call 与 delegatecall 的区别示例
我们通过一个对比实验,直观理解两者在执行环境中的差异。
编写并部署如下的 A 合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract A {
address public a;
function test() public returns (address b) {
b = address(this); // 获取当前合约地址
a = b; // 保存到状态变量 a 中
}
}
合约部署完成后,记录合约 A 地址为 0xcD6a42782d230D7c13A74ddec5dD140e55499Df9。
我们将 A 合约地址填入 B 合约中,并部署如下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract B {
address public a;
// A 合约地址
address Aaddress = ad