Solidity 中的 call 调用
call
是一种底层函数调用方式,用于与其他合约进行交互, 分为 3 种:
call
staticcall
delegatecall
call
可以用来调用另一个合约的函数,或直接发送以太币, 它的语法如下:
(bool success, bytes memory returnData) = address(target).call{value: msg.value, gas: gasAmount}(data);
使用示例
- 调用其他合约函数
pragma solidity ^0.8.0;
// 目标合约
contract TargetContract {
uint256 public value;
function setValue(uint256 _value) external payable {
value = _value;
}
}
// 调用方合约
contract CallerContract {
function callSetValue(address target, uint256 _value) public returns (bool) {
// abi.encodeWithSignature("setValue(uint256)", _value) 会生成目标合约的函数签名和参数数据。
// target.call(...) 会调用 TargetContract 中的 setValue 函数
(bool success, ) = target.call(abi.encodeWithSignature("setValue(uint256)", _value));
return success;
}
}
// 在某些情况下, 目标合约的 ABI 可能是未知的
// 用户可以动态传入 data(即函数签名和参数的 ABI 编码), 这样就可以调用目标合约的任意函数,而不需要明确的接口定义
contract CallerContract {
function callUnknownFunction(address target, bytes memory data) public returns (bool, bytes memory) {
(bool success, bytes memory returnData) = target.call(data);
return (success, returnData);
}
}
- 发送以太币
pragma solidity ^0.8.0;
// 接收方合约
contract TargetContract {
uint256 public balance;
receive() external payable {
balance += msg.value;
}
}
// 调用方合约
pragma solidity ^0.8.0;
contract CallerContract {
function sendEther(address payable target) public payable returns (bool) {
// target.call{value: msg.value}("") 会向目标合约发送以太币。
// msg.value 是通过交易传入的以太币数量
(bool success, ) = target.call{value: msg.value}("");
return success;
}
}
底层实现
call 的 EVM 指令流程如下:
1. 初始化调用:
- 执行
CALL
指令。 - 将调用的参数(包括 to 地址、value、gas、calldata 等)压入 EVM 的堆栈。
- 创建新的执行上下文,分配给目标合约。
2. 切换上下文:
- 当前 EVM 上下文切换到目标合约。
- 使用目标合约的代码和存储。
- 在执行目标合约逻辑时,msg.sender 和 msg.value 保持调用合约传递的值。
3. 完成调用:
- 执行完成后,返回执行结果和返回数据。
- 如果调用失败,调用者合约需要手动处理错误(success = false)
注意事项
- call 不会抛出异常,即使调用失败也不会回滚。因此,调用者需要检查返回的 success 值,并根据需要手动处理错误
(bool success, bytes memory data) = target.call(data);
require(success, "Low-level call failed");
-
Reentrancy
攻击 由于 call 是低级调用,当目标合约执行其逻辑时,可能会调用当前合约的代码,导致 reentrancy(重入攻击), 建议使用 checks-effects-interactions 模式,先修改状态,再执行外部调用,或者使用 Solidity 0.8.0+ 的 reentrancy guard -
Gas 限制 call 提供了一个 gas 限制参数。如果不指定,目标合约继承调用者剩余的 Gas。如果目标合约消耗了太多的 Gas,可能导致调用失败
staticcall
(bool success, bytes memory returnData) = target.staticcall(data);
静态调用,用于执行只读操作,调用的目标合约代码不能修改状态,也不能发送以太币
底层实现
staticcall 的 EVM 指令流程如下:
1. 初始化调用:
- 执行
STATICCALL
指令。 - 将调用参数(目标地址、calldata 等)压入 EVM 堆栈。
- 初始化只读上下文,目标合约无法更改状态
2. 执行静态逻辑:
- 当前 EVM 上下文切换到目标合约。
- 执行的代码仅允许查询变量或返回数据。
3. 完成调用:
- 执行完成后,返回调用结果和返回数据。
- 如果调用尝试修改状态,staticcall 会直接失败
注意事项
- 状态修改限制:
staticcall
不能调用会修改状态的函数,否则调用会失败。 - staticcall 的 Gas 消耗通常较少,因为它只执行只读操作
delegatecall
(bool success, bytes memory returnData) = target.delegatecall(data);
- 和
call
类似, 区别在于一个合约对目标智能合约进行 delegatecall 时,会在自己的环境中执行目标合约的逻辑 (相当于把目标合约的代码复制到当前合约中执行) delegatecall
是可升级合约架构(如 透明代理模式 和 UUPS 模式)的核心
底层实现
delegatecall 的 EVM 指令流程如下:
1. 初始化调用:
- 执行
DELEGATECALL
指令。 - 将调用参数(目标地址、gas、calldata 等)压入 EVM 堆栈。
- 不发送 value,只传递执行逻辑。
- 分配执行上下文,但使用调用合约的存储
2. 切换逻辑上下文:
- 当前 EVM 上下文切换到目标合约的代码逻辑。
- 所有的存储变量、msg.sender 和 msg.value 都保持为调用者合约的值。
3. 完成调用:
- 执行完成后,返回调用结果和返回数据。
- 如果调用失败,success 为 false
注意事项
- 存储与上下文继承:目标合约的存储变量不会被修改,所有存储操作都应用在调用合约(当前合约)中。
- 如果目标合约的存储布局与代理合约不匹配,可能导致意外的存储冲突。
- 安全性:delegatecall 执行目标合约逻辑,但使用调用者的上下文。如果目标合约中有恶意代码,可能导致调用者的存储被破坏
总结
特性 | call |
delegatecall |
staticcall |
---|---|---|---|
调用上下文 | 使用目标合约的存储和上下文 | 使用调用者合约的存储和上下文 | 使用目标合约的存储和上下文 |
状态修改 | 可以修改 | 可以修改 | 不允许(只读操作) |
Gas 消耗 | 依赖目标合约代码 | 依赖目标合约代码 | 较低,只执行只读逻辑 |
用途 | 调用函数、发送以太币 | 实现可升级合约,调用共享逻辑 | 安全查询、静态调用 |
返回值 | 成功布尔值和返回数据 | 成功布尔值和返回数据 | 成功布尔值和返回数据 |
风险 | 易受重入攻击 | 如果存储布局不匹配,可能引发存储冲突 | 安全,无状态修改 |
注: 从底层实现来看, 三种方式分别对应三个不同的 EVM 指令,其中最主要的区别也是在初始化上下文:
call
是创建新的执行上下文,分配给目标合约staticcall
是初始化只读上下文,目标合约无法更改状态delegatecall
是分配执行上下文,但使用调用合约自己本身的存储