静态调用 staticcall

本章讲解在 Solidity 中,如何使用 staticcall 来调用其它合约的函数。

官网binschool.app
推特@BinSchool    DiscordBinDAO   微信:bkra50 

在 Solidity 中,staticcall 是一个用于在智能合约中调用外部合约函数的一种方式。

staticcall 是一个低级别的操作,它允许一个合约在调用外部合约函数时,仅限于读取外部合约的数据而不修改它的状态。

也就是说,staticcall 的只能调用外部合约的视图函数和纯函数,即函数的状态可变性为 viewpure 函数。

staticcall 的使用方法,如下所示:

contract StaticCaller {
  function foo(address externalAddress, bytes memory data) 
    external view returns (bool, bytes memory) {
    (bool success, bytes memory returnData) = externalAddress.staticcall(data);
     return (success, returnData);
  }
}

在上面的例子中,StaticCaller 合约中的 foo 函数中,使用了 staticcall

它接受一个外部合约的地址  externalAddress 和一个字节数组 data,这个数组包含了要调用的外部合约函数的数据。

staticcall 返回了两个值:一个是布尔值,表示调用是否成功;另一个是 bytes 类型的数据,包含调用的返回值。

需要注意的是,staticcall 的限制是它不能修改合约的状态。如果尝试在 staticcall 中调用会改变状态的函数,会导致异常并且返回失败。

1. staticcall 实现原理

staticcallEVM 中的一条指令,指令代码是 0xfa。 当执行 staticcall 调用一个外部合约的函数时,它会将 EVM 解释器的状态 readonly 置为 true

func (evm *EVM) StaticCall(....) (ret []byte, leftOverGas uint64, err error) {
  .....
  ret, err = evm.interpreter.Run(contract, input, true) /*readonly=true*/
  ....
}

EVM 执行外部合约的函数时,如果解释器的状态 readonly 为 true,那么该函数就不能执行状态变量存储指令 opSstore。也就是说,该外部合约的函数不能改变合约状态。

func opSstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
  ....
  if interpreter.readOnly {
    return nil, ErrWriteProtection
  }
  ....
}

2. 示例合约

下面是一个使用静态调用 staticcall 的示例合约:

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

// 被调用合约
contract Callee {
  // 被调用函数
  function bar() external pure returns(uint256) {
    return 1;
  }
}

// 静态调用函数
contract StaticCaller {
  function foo(address externalAddress) 
    external view returns (bool, bytes memory) {
    // 构造bar() 的函数选择器
    bytes memory data = abi.encodeWithSignature("bar()");
    // 静态调用合约 externalAddress 的函数 bar
    (bool success, bytes memory returnData) = externalAddress.staticcall(data);
     return (success, returnData);
  }
}

我们将上面的合约复制到 Remix,先进行编译。

编译后会有两个合约 CalleeStaticCaller,我们要首先部署被调用合约 Callee,然后再部署静态调用合约 StaticCaller

我们将 Callee 合约的地址填写到 StaticCallerfoo 参数位置,然后点击 foo 按钮进行调用。

我们可以看到调用结果为 true,表示调用成功。而被调用合约 Callee 的函数 bar,返回了结果值 1。