笔者记录这些问题的原因:solidity更新很快,才几个月没使用,现在使用最新版(0.5.10)使用call
调用合约的时候,发现大变样。。。
这种方式调用,是最简单方便的调用方式,缺点就是这能调用固定的接口,不够灵活。
先直接上代码。示例都是调用一个已经部署的合约的deposit
方法。
pragma solidity ^0.5.10;
interface ContractInterface {
function deposit(string calldata _name) external payable returns(bool);
}
contract InterfaceCall {
function callDeposit(address _contract, string memory _args)
public
returns(bool)
{
ContractInterface ci = ContractInterface(_contract);
bool retValue = ci.deposit(_args);
return retValue;
}
}
通用型调用一般直接使用call
方法调用。这种方式调用,是灵活的调用方式,缺点就是太灵活导致生产环境中出现了很多重大bug,使用此种调用方式,请明确风险。
先上代码。
警告: 生成环境中一定要处理call的返回值!!!生成环境中一定要处理call的返回值!!生成环境中一定要处理call的返回值!!重要的事情说3遍。。。
pragma solidity ^0.5.10;
contract CallContract {
/**
* @dev 无限制调用指定合约的方法。
* @param _contract address 被调用的合约的部署地址
* @param _func string 方法的声明
* @param _args string 方法的参数
*/
function callFunc(address _contract, string memory _func, string memory _args)
public
returns(bytes memory)
{
// 1. 获取函数的签名
bytes4 signature = bytes4(keccak256(abi.encodePacked(_func)));
// 2. 把函数签名和参数通过 encodeWithSelector 压缩成一个 bytes
bytes memory _calldata = abi.encodeWithSelector(signature, _args);
// 3. 调用函数的方法
(bool success, bytes memory returnData) = _contract.call(_calldata);
// 4. 处理返回值。
require(success == true, "call failure");
return returnData;
}
}
直接从Remix编译结果中取。 编译界面的Compilation Details
中的 FUNCTIONHASHES
// "a26e1186": "deposit(string)",
bytes4 selector = 0xa26e1186;
利用abi编码方法计算。
function calculateSign(string memory _func) public pure returns(bytes4) {
return bytes4(keccak256(abi.encodePacked(_func)));
}
原理:函数签名是函数声明的Keccak-256
计算的前4个字节。
函数名字(参数类型1,参数类型N)
,和参数名称无关。通过
keccak256(bytes)
方法对函数声明进行计算,取前4个字节,就是我们需要的函数签名。因为此hash函数的参数是bytes
,所有我们先需要通过abi.encoePacked
把string类型转换成bytes
类型
由于call(bytes calldata) returns(bool, bytes)
函数方法的改变,过去直接传递函数签名和参数的方法不再可行。需要先通过abi.encodeWithSelector(byte4 selector, ...)
把函数签名和参数编码成bytes
类型
bytes memory _calldata = abi.encodeWithSelector(selector, _args);
也可以把第一步和第二步合起来写,使用另外一个内置方法encodeWithSignature
直接把函数声明和参数直接打包成一个bytes:
/**
* @dev 无限制调用指定合约的方法。
* @param _contract address 被调用的合约的部署地址
* @param _func string 方法的声明
* @param _args string 方法的参数
*/
function callFunc(address _contract, string memory _func, string memory _args)
public
returns(bytes memory)
{
bytes memory _calldata = abi.encodeWithSignature(_func, _args);
(bool success, bytes memory returnData) = _contract.call(_calldata);
require(success == true, "call failure");
return returnData;
}
现在可以通过这种方式调用:
(bool success, bytes memory returnData) = _contract.call(_calldata);
示例中只是简单的处理一下返回值,此处是以太坊智能合约的一个大坑。请一定要处理call的返回值。
有些情况下,要限制调用的身份,不让合约调用,只允许普通账户调用接口,避免以下漏洞和薅羊毛的行为。只通过查看地址的代码空间的长度来判断,是不能防止合约调用的(可以在部署合约的构造函数中调用,此时它的代码空间还是0,可以跳过这个限制)。
modifier isHuman() {
address _addr = msg.sender;
uint256 _codeLength;
assembly {_codeLength := extcodesize(_addr)}
require(_codeLength == 0, "sorry humans only");
require(_addr == tx.origin);
_;
}