筆者記錄這些問題的原因: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;
}
}
(1) 獲取函數簽名
- 方法一:
直接從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
類型
(2) 打包簽名和參數
由於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;
}
(3) 調用合約
現在可以通過這種方式調用:
(bool success, bytes memory returnData) = _contract.call(_calldata);
(4) 處理返回值
示例中只是簡單的處理一下返回值,此處是以太坊智能合約的一個大坑。請一定要處理call的返回值。
三、安全問題
(1) 限制合約調用
有些情況下,要限制調用的身份,不讓合約調用,只允許普通賬戶調用接口,避免以下漏洞和薅羊毛的行爲。只通過查看地址的代碼空間的長度來判斷,是不能防止合約調用的(可以在部署合約的構造函數中調用,此時它的代碼空間還是0,可以跳過這個限制)。
modifier isHuman() {
address _addr = msg.sender;
uint256 _codeLength;
assembly {_codeLength := extcodesize(_addr)}
require(_codeLength == 0, "sorry humans only");
require(_addr == tx.origin);
_;
}