Solidity 支持一个合约调用另一个合约。两个合约既可以位于同一文件内,也可以位于不同的两个文件中。还能调用已经上链的其它合约。
- 调用内部合约
- 内部合约指:位于同一 sol 文件中的合约,它们不需要额外的声明就可以直接调用。
- 调用外部合约
- 外部合约指:位于不同文件的外部合约,以及上链的合约。
- 方法一: 通过接口方式调用
- 方法二: 通过签名方式调用
了解上面的调用后,可以扩展了解多次调用
地址转换为合约对象的防范:
- 方法 1: 通过
ContractName(_ads)
将传入的地址,转为合约对象Test(_ads).setX(_x);
- 如果为了代码逻辑,也可以分开写,比如
Test temp = Test(_ads); temp.setX(_x);
- 方法 2: 可以通过参数中指定合约名字进行转换
function setX2(Test _ads, uint256 _x) public { _ads.setX(_x); }
- 调用并发送 ETH:
fnName{value: msg.value}();
Test(_ads).setYBySendEth{value: msg.value}();
例子演示:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Test {
uint256 public x = 1;
uint256 public y = 2;
function setX(uint256 _x) public {
x = _x;
}
function getX() public view returns (uint256) {
return x;
}
function setYBySendEth() public payable {
y = msg.value;
}
function getXandY() public view returns (uint256, uint256) {
return (x, y);
}
}
contract CallTest {
// 第1种方法: 229647 / 27858 gas
function setX1(address _ads, uint256 _x) public {
Test(_ads).setX(_x);
}
// 第2种方法: 27923 gas
function setX2(Test _ads, uint256 _x) public {
_ads.setX(_x);
}
function getX(address _ads) public view returns (uint256) {
return Test(_ads).getX();
}
function setYBySendEth(address _ads) public payable {
Test(_ads).setYBySendEth{value: msg.value}();
}
function getXandY(address _ads)
public
view
returns (uint256 __x, uint256 __y)
{
(__x, __y) = Test(_ads).getXandY();
}
}
核心代码
interface AnimalEat {
function eat() external returns (string memory);
}
contract Animal {
function test(address _addr) external returns (string memory) {
AnimalEat general = AnimalEat(_addr);
return general.eat();
}
}
通过签名方式调用合约,只需要传入被调用者的地址和调用方法声明。
在第二章地址类型那一节有详细的介绍
- 使用 call
- 使用 delegatecall
- 使用 staticcall
call 核心代码如下
bytes memory data = abi.encodeWithSignature(
"setNameAndAge(string,uint256)",
_name,
_age
);
(bool success, bytes memory _bys) = _ads.call{value: msg.value}(data);
require(success, "Call Failed");
bys = _bys;
用给定的有效载荷(payload)发出低级 CALL
调用,并返回交易成功状态和返回数据(调用合约的方法并转账), 格式如下:
<address>.call(bytes memory) returns (bool, bytes memory)
DelegateCall 核心代码如下
- 委托调用后,所有变量修改都是发生在委托合约内部,并不会保存在被委托合约中。
- 利用这个特性,可以通过更换被委托合约,来升级委托合约。
- 委托调用合约内部,需要和被委托合约的内部参数完全一样,否则容易导致数据混乱
- 可以通过顺序来避免这个问题,但是推荐完全一样
function set(address _ads, uint256 _num) external payable {
sender = msg.sender;
value = msg.value;
num = _num;
// 第1种 encode
// 不需知道合约名字,函数完全自定义
bytes memory data1 = abi.encodeWithSignature("set(uint256)", _num);
// 第2种 encode
// 需要合约名字,可以避免函数和参数写错
// bytes memory data2 = abi.encodeWithSelector(Test1.set.selector, _num);
(bool success, bytes memory _data) = _ads.delegatecall(data1);
require(success, "DelegateCall set failed");
}
staticcall 核心代码如下: 它与 call 基本相同,但如果被调用的函数以任何方式修改状态变量,都将回退。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
// 被调用的合约
contract Hello {
function echo() external pure returns (string memory) {
return "Hello World!";
}
}
// 调用者合约
contract SoldityTest {
function callHello(address _ads) external view returns (string memory) {
// 编码被调用者的方法签名
bytes4 methodId = bytes4(keccak256("echo()"));
// 调用合约
(bool success, bytes memory data) = _ads.staticcall(
abi.encodeWithSelector(methodId)
);
if (success) {
return abi.decode(data, (string));
} else {
return "error";
}
}
}
- 把多个合约的多次函数的调用,打包在一个里面对合约进行调用。RPC 对调用有限制,这样可以绕开限制。
- 多次调用里面,对方的内部,
msg.sender
是 MultiCall 合约,而不是用户地址。
- 调用的地址
- 调用的 data
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Test {
function fn1()
external
view
returns (
uint256,
address,
uint256
)
{
return (1, msg.sender, block.timestamp);
}
function fn2()
external
view
returns (
uint256,
address,
uint256
)
{
return (2, msg.sender, block.timestamp);
}
function getFn1Data() external pure returns (bytes memory) {
// 两种签名方法都可以
// abi.encodeWithSignature("fn1()");
return abi.encodeWithSelector(this.fn1.selector);
}
function getFn2Data() external pure returns (bytes memory) {
return abi.encodeWithSelector(this.fn2.selector);
}
}
contract MultiCall {
function multiCall(address[] calldata targets, bytes[] calldata data)
external
view
returns (bytes[] memory)
{
require(targets.length == data.length, "targets.length != data.length");
bytes[] memory results = new bytes[](data.length);
for (uint256 index = 0; index < targets.length; index++) {
(bool success, bytes memory result) = targets[index].staticcall(
data[index]
);
require(success, "call faild");
results[index] = result;
}
return results;
}
}
测试
-
部署
Test
:0x1c91347f2A44538ce62453BEBd9Aa907C662b4bD
- 使用
getFn1Data
获取 fn1 data - 使用
getFn2Data
获取 fn2 data
- 使用
-
部署
MultiCall
:0x93f8dddd876c7dBE3323723500e83E202A7C96CC
-
调用 multiCall 方法
- 参数 1:
["Test 地址","Test 地址"]
- 参数 2:
["fn1 data","fn2 data"]
- 参数 1:
-
返回值如下
0x 0000000000000000000000000000000000000000000000000000000000000001 00000000000000000000000093f8dddd876c7dbe3323723500e83e202a7c96cc 00000000000000000000000000000000000000000000000000000000630c7834, 0x 0000000000000000000000000000000000000000000000000000000000000002 00000000000000000000000093f8dddd876c7dbe3323723500e83e202a7c96cc 00000000000000000000000000000000000000000000000000000000630c7834
为什么使用 MultiDelegatecall ,不使用 MultiCall?是为了让被调用的合约内,msg.sender
是用户合约,而不是中转合约的地址。
但是委托调用的缺点是,合约必须是自己编写的,不能是别人编写的。
多次委托调用,存在漏洞,不要在里面多次累加余额。或者多重委托禁止接受资金。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract MultiDelegatecall {
function multiDelegatecall(bytes[] calldata data)
external
returns (bytes[] memory)
{
bytes[] memory results = new bytes[](data.length);
for (uint256 index = 0; index < data.length; index++) {
(bool success, bytes memory result) = address(this).delegatecall(
data[index]
);
require(success, "call faild");
results[index] = result;
}
return results;
}
}
contract Test is MultiDelegatecall {
function fn1()
external
view
returns (
uint256,
address,
uint256
)
{
return (1, msg.sender, block.timestamp);
}
function fn2()
external
view
returns (
uint256,
address,
uint256
)
{
return (2, msg.sender, block.timestamp);
}
function getFn1Data() external pure returns (bytes memory) {
// 两种签名方法都可以
// abi.encodeWithSignature("fn1()");
return abi.encodeWithSelector(this.fn1.selector);
}
function getFn2Data() external pure returns (bytes memory) {
return abi.encodeWithSelector(this.fn2.selector);
}
}
合约测试
-
部署 Test 合约
-
获取 getFn1Data:
0x648fc804
-
获取 getFn2Data:
0x98d26a11
-
调用
multiDelegatecall
- ["0x648fc804","0x98d26a11"]
-
得到 decoded output,发现地址是用户的
0x 0000000000000000000000000000000000000000000000000000000000000001 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 00000000000000000000000000000000000000000000000000000000630c8ebc, 0x 0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 00000000000000000000000000000000000000000000000000000000630c8ebc
- 内部合约调用有哪些方法?
- 方法 1: 通过
ContractName(_ads)
将传入的地址,转为合约对象Test(_ads).setX(_x);
- 如果为了代码逻辑,也可以分开写,比如
Test temp = Test(_ads); temp.setX(_x);
- 方法 2: 可以通过参数中指定合约名字进行转换
function setX2(Test _ads, uint256 _x) public { _ads.setX(_x); }
- 调用并发送 ETH:
fnName{value: msg.value}();
Test(_ads).setYBySendEth{value: msg.value}();
- 方法 1: 通过
- 调用外部合约有哪些方法?
- 1 通过接口方式调用
interface AnimalEat { function eat() external returns (string memory); } contract Animal { function test(address _addr) external returns (string memory) { AnimalEat general = AnimalEat(_addr); return general.eat(); } }
- 2 通过签名方式调用(call/delegatecall/staticcall)
- call 核心代码如下
bytes memory data = abi.encodeWithSignature( "setNameAndAge(string,uint256)", _name, _age ); (bool success, bytes memory _bys) = _ads.call{value: msg.value}(data); require(success, "Call Failed"); bys = _bys;
- 用给定的有效载荷(payload)发出低级
CALL
调用,并返回交易成功状态和返回数据(调用合约的方法并转账), 格式如下:
<address>.call(bytes memory) returns (bool, bytes memory)
-
DelegateCall 核心代码如下
function set(address _ads, uint256 _num) external payable { sender = msg.sender; value = msg.value; num = _num; // 第1种 encode // 不需知道合约名字,函数完全自定义 bytes memory data1 = abi.encodeWithSignature("set(uint256)", _num); // 第2种 encode // 需要合约名字,可以避免函数和参数写错 // bytes memory data2 = abi.encodeWithSelector(Test1.set.selector, _num); (bool success, bytes memory _data) = _ads.delegatecall(data1); require(success, "DelegateCall set failed"); }
-
staticcall 核心代码如下: 它与 call 基本相同,但如果被调用的函数以任何方式修改状态变量,都将回退。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; // 被调用的合约 contract Hello { function echo() external pure returns (string memory) { return "Hello World!"; } } // 调用者合约 contract SoldityTest { function callHello(address _ads) external view returns (string memory) { // 编码被调用者的方法签名 bytes4 methodId = bytes4(keccak256("echo()")); // 调用合约 (bool success, bytes memory data) = _ads.staticcall( abi.encodeWithSelector(methodId) ); if (success) { return abi.decode(data, (string)); } else { return "error"; } } }
- MultiCall/多次调用
- 把多个合约的多次函数的调用,打包在一个里面对合约进行调用。RPC 对调用有限制,这样可以绕开限制。
- 多次调用里面,对方的内部,
msg.sender
是 MultiCall 合约,而不是用户地址。
contract MultiCall { function multiCall(address[] calldata targets, bytes[] calldata data) external view returns (bytes[] memory) { require(targets.length == data.length, "targets.length != data.length"); bytes[] memory results = new bytes[](data.length); for (uint256 index = 0; index < targets.length; index++) { (bool success, bytes memory result) = targets[index].staticcall( data[index] ); require(success, "call faild"); results[index] = result; } return results; } }
- MultiDelegatecall / 多次委托调用
- 为什么使用 MultiDelegatecall ,不使用 MultiCall?是为了让被调用的合约内,
msg.sender
是用户合约,而不是中转合约的地址。但是委托调用的缺点是,合约必须是自己编写的,不能是别人编写的。多次委托调用,存在漏洞,不要在里面多次累加余额。或者多重委托禁止接受资金。// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; contract MultiDelegatecall { function multiDelegatecall(bytes[] calldata data) external returns (bytes[] memory) { bytes[] memory results = new bytes[](data.length); for (uint256 index = 0; index < data.length; index++) { (bool success, bytes memory result) = address(this).delegatecall( data[index] ); require(success, "call faild"); results[index] = result; } return results; } } contract Test is MultiDelegatecall { function fn1() external view returns ( uint256, address, uint256 ) { return (1, msg.sender, block.timestamp); } function fn2() external view returns ( uint256, address, uint256 ) { return (2, msg.sender, block.timestamp); } function getFn1Data() external pure returns (bytes memory) { // 两种签名方法都可以 // abi.encodeWithSignature("fn1()"); return abi.encodeWithSelector(this.fn1.selector); } function getFn2Data() external pure returns (bytes memory) { return abi.encodeWithSelector(this.fn2.selector); } }
- 为什么使用 MultiDelegatecall ,不使用 MultiCall?是为了让被调用的合约内,