Solidity合約調合約那些事

筆者記錄這些問題的原因: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);
        _;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章