0%

call、staticcall、delegatecall使用

在 Solidity 中,callstaticcalldelegatecall是强大的低级操作,用于与其他合约交互或在当前上下文中执行外部合约的逻辑。掌握它们的用法和区别对于智能合约开发非常重要。

本文将详细介绍它们的功能、适用场景、代码示例,以及使用时需要注意的潜在问题。

call

功能

call 是一种通用方法,用于调用另一个合约的函数或发送 ETH。它可以调用目标合约的任意函数,包括不存在的函数(在这种情况下不会抛出错误)。

特点

  • 可读写目标合约的状态。
  • 支持附带 ETH 发送。
  • 返回两个值:
    • 调用是否成功 (bool)。
    • 调用返回的数据 (bytes memory)。

使用场景

  • 调用外部合约的任意函数。
  • 向合约或外部账户发送 ETH。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.8.0;

contract CallExample {
function callFunction(address target, uint256 value) external returns (bool, bytes memory) {
// 通过 call 调用目标合约的 setValue(uint)
(bool success, bytes memory data) = target.call(
abi.encodeWithSignature("setValue(uint256)", value)
);
require(success, "Call failed");
return (success, data);
}

function sendEth(address payable target) external payable {
// 使用 call 发送 ETH
(bool success, ) = target.call{value: msg.value}("");
require(success, "Send ETH failed");
}
}

staticcall

功能

staticcall是一种只读调用方法,用于调用目标合约的视图或纯函数。它不允许改变状态,因此更安全且节省 Gas

特点

  • 只能调用 view 或 pure 修饰的函数。
  • 无法修改状态或发送 ETH。
  • 返回两个值:
    • 调用是否成功 (bool)。
    • 调用返回的数据 (bytes memory)。

使用场景

  • 查询目标合约的状态。
  • 调用只读逻辑以避免状态改变。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.8.0;

contract StaticCallExample {
function staticCallFunction(address target) external view returns (uint256) {
// 使用 staticcall 调用目标合约的 storedValue()
(bool success, bytes memory data) = target.staticcall(
abi.encodeWithSignature("storedValue()")
);
require(success, "Staticcall failed");
return abi.decode(data, (uint256));
}
}

delegatecall

功能

delegatecall 是一种在当前合约上下文中执行目标合约逻辑的方法。它会以调用合约的存储布局为准,执行目标合约中的代码。

特点

  • 使用调用合约的存储。
  • 使用调用合约的 msg.sender 和 msg.value。
  • 返回两个值:
    • 调用是否成功 (bool)。
    • 调用返回的数据 (bytes memory)。

使用场景

  • 实现合约代理(如升级逻辑)。
  • 在共享存储布局的上下文中执行外部逻辑。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.8.0;

contract DelegateCallExample {
uint256 public storedValue;

function delegateCallFunction(address target, uint256 value) external {
// 使用 delegatecall 调用目标合约的 setValue(uint)
(bool success, ) = target.delegatecall(
abi.encodeWithSignature("setValue(uint256)", value)
);
require(success, "Delegatecall failed");
}
}

// 被调用合约 (Library)
contract Target {
uint256 public storedValue;

function setValue(uint256 value) external {
storedValue = value
}
}

call VS staticcall VS delegatecall

特性 call staticcall delegatecall
可读写状态 ❌(只读)
使用调用合约存储
使用调用合约msg.sender
支持发送ETH
调用不存在的函数 支持,返回失败 支持,返回失败 支持,返回失败

使用注意事项

call 的安全性

  • 使用 call 调用外部合约时,目标合约可能会执行恶意代码。需要额外检查返回值并限制权限。
  • 不要轻易使用 call 调用不可信的合约。

delegatecall 的存储风险

  • 调用目标合约时,目标合约必须与调用合约共享相同的存储布局,否则可能导致存储冲突。
  • 使用 delegatecall 需要确保调用的是受信任的逻辑。

Gas 消耗与返回值处理

  • 注意低级调用的 Gas 使用,避免由于 Gas 不足导致调用失败。
  • 低级调用(call、staticcall 和 delegatecall)不会自动抛出异常,需要手动处理返回值。

不要使用简写类型

无论是使用abi.encodeWithSelect 还是 abi.encodeWithSignature ,参数中方法名后的参数一定不要使用简写类型。

1
2
// 错误写法
(bool success, ) = target.call(abi.encodeWithSignature("setValue(uint)", value));

因为在编译的过程中,编译器会将简写类型(如:uint)转换成uint256,这将导致,你的签名或选择器不匹配,导致执行失败。

实践案例:实现代理合约

以下是使用 delegatecall 的代理合约的示例,用于实现合约逻辑的动态升级

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.8.0;

// 逻辑合约
contract LogicContract {
uint256 public storedValue;

function setValue(uint256 value) external {
storedValue = value;
}
}

// 代理合约
contract ProxyContract {
address public logicContract;

constructor(address _logicContract) {
logicContract = _logicContract;
}

fallback() external payable {
(bool success, ) = logicContract.delegatecall(msg.data);
require(success, "Delegatecall failed");
}
}

运行过程

  • 部署 LogicContract。
  • 部署 ProxyContract,将 LogicContract 的地址传递给其构造函数。
  • 通过代理合约调用 setValue 方法,storedValue 实际存储在 ProxyContract 中

结论

callstaticcalldelegatecall 是 Solidity 中的基础工具,可以用来实现灵活的合约交互和逻辑扩展。在使用这些低级调用时,需要仔细处理返回值、安全性以及存储一致性问题。

通过合理使用这些工具,您可以设计功能强大且可扩展的智能合约体系,同时避免潜在的安全漏洞。