合约调用合约

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.18;

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 核心代码如下

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.18;

// 被调用的合约
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 合约,而不是用户地址。

说明

  • 调用的地址

  • 调用的 data

合约代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

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"]

  • 返回值如下

    0x
    0000000000000000000000000000000000000000000000000000000000000001
    00000000000000000000000093f8dddd876c7dbe3323723500e83e202a7c96cc
    00000000000000000000000000000000000000000000000000000000630c7834,
    0x
    0000000000000000000000000000000000000000000000000000000000000002
    00000000000000000000000093f8dddd876c7dbe3323723500e83e202a7c96cc
    00000000000000000000000000000000000000000000000000000000630c7834
    

MultiDelegatecall / 多次委托调用

为什么使用 MultiDelegatecall ,不使用 MultiCall?是为了让被调用的合约内,msg.sender 是用户合约,而不是中转合约的地址。

但是委托调用的缺点是,合约必须是自己编写的,不能是别人编写的。

多次委托调用,存在漏洞,不要在里面多次累加余额。或者多重委托禁止接受资金。

合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

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 通过接口方式调用

      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.18;
        
          // 被调用的合约
          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.18;
      
      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);
          }
      }