第十三課 SOLIDITY語法難點解析及故障排查

筆名輝哥發表於2018-11-15

第十三課 SOLIDITY語法難點解析及故障排查

1.編輯器說明

(1)推薦編輯器 目前嘗試 Solidity 程式設計的最好的方式是使用 Remix (需要時間載入,請耐心等待)。Remix 是一個基於 Web 的 IDE,它可以讓你編寫 Solidity 智慧合約,然後部署並執行該智慧合約。 如果外網不能訪問,可以訪問歐陽哥哥搭建的REMIX編輯器 (2)Visual Studio Extension Microsoft Visual Studio 的 Solidity 外掛,包含 Solidity 編譯器。 (3)Visual Studio Code extension Microsoft Visual Studio Code 外掛,包含語法高亮和 Solidity 編譯器。

2. REMIN的函式引用

function mint(address receiver, uint amount)

(1) 在REMIX輸入時,地址一定要有""表示,否則amount就取不到值。 例如是mint("0xca35b7d915458ef540ade6068dfe2f44e8fa733c",101) (2) 程式中,如果註釋包含中文,單步除錯的位置不準確。

3.address相關全域性函式

.balance (uint256): 該地址有多少以太坊餘額(wei為單位)

.transfer(uint256 amount): 傳送特定數量(wei為單位)的以太坊到對應地址,當出現錯誤時會扔出異常,但不會因異常而停止。固定需要耗費2300個gas。

.send(uint256 amount) returns (bool): 傳送特定數量(wei為單位)的以太坊到對應地址,當出現錯誤時會返回flase。固定需要耗費2300個gas。

【警告】send() 執行有一些風險:如果呼叫棧的深度超過1024或gas耗光,交易都會失敗。因此,為了保證安全,必須檢查send的返回值,如果交易失敗,會回退以太幣。如果用transfer會更好。

.call(...) returns (bool): CALL的低階呼叫函式,當失敗時返回false。執行需要消耗不固定的gas。 【說明】不鼓勵使用call函式,後期將會被移除。呼叫該函式可能造成安全攻擊,詳見後期安全相關文章。

.callcode(...) returns (bool): CALLCODE的低階呼叫函式,當失敗時返回false。執行需要消耗不固定的gas。 不建議使用,後續版本會刪除。

.delegatecall(...) returns (bool): DELEGATECALL的低階呼叫函式,當失敗時返回false。執行需要消耗不固定的gas。 **【說明】**為了和非ABI協議的合約進行互動,可以使用call() 函式, 它用來向另一個合約傳送原始資料,支援任何型別任意數量的引數,每個引數會按規則(ABI協議)打包成32位元組並一一拼接到一起。一個例外是:如果第一個引數恰好4個位元組,在這種情況下,會被認為根據ABI協議定義的函式器指定的函式簽名而直接使用。如果僅想傳送訊息體,需要避免第一個引數是4個位元組。如下面的例子:

function callfunc(address addr) returns (bool){ bytes4 methodId = bytes4(keccak256("setScore(uint256)")); return addr.call(methodId, 100); }

測試地址和地址呼叫程式碼舉例

pragma solidity ^0.4.18;

contract AddrTest{
    /*event函式知識把引數資訊列印在REMIX等編譯器的LOG位置區,不需要函式定義。*/
    event logdata(bytes data);
    event LogContractAddress(address exAccount, address contractAddress);
    
    uint score = 0;
    
    /*回撥函式,沒有函式名。任何呼叫不存在的函式,這時被呼叫的合約的fallback函式會執行。
     payable:如果一個函式需要進行貨幣操作,必須要帶上payable關鍵字*/
    function() payable {
        logdata(msg.data);
    }
    
    /*智慧合約構建函式*/
    function AddrTest(){
    LogContractAddress(msg.sender,this);
    }

    function getBalance() returns (uint) {
        return this.balance;
    }


    function setScore(uint s) public {
        score = s;
    }

    function getScore() returns ( uint){
        return score;
    }
}

contract CallTest{
    /*該函式有函式申明沒有實際函式內容,在remix的value區域設定以太坊的個數,呼叫該函式會把外部賬戶(ACCOUNT)中的
      以太坊轉移到智慧合約賬戶中*/
    function deposit() payable {
    }

    event logSendEvent(address to, uint value);
    event LogContractAddress(address exAccount, address contractAddress);
    
    
    /*轉以太坊給目標地址*/
    function transferEther(address towho) payable {
        towho.transfer(10);/*單位為wei*/
        
        logSendEvent(towho, 10);
    }
   
    /*不指定呼叫函式,則呼叫無函式名的回撥函式*/
    function callNoFunc(address addr) returns (bool){
        return addr.call("tinyxiong", 1234);
    }

    /*制定呼叫函式的方法*/
    function callfunc(address addr) returns (bool){
        bytes4 methodId = bytes4(keccak256("setScore(uint256)"));
        return addr.call(methodId, 100);
    }  

    /*返回當前合約的以太坊餘額*/
    function getBalance() returns (uint) {
        return this.balance;//0
    }  
    
    /*銷燬智慧合約,把以太坊餘額返回給當前外部賬戶*/
    function ContractSuide() {
        LogContractAddress(this,msg.sender);
        suicide(msg.sender);
    }
}
複製程式碼

4.Contract Related

this (current contract’s type): 表示當前合約,可以顯式的轉換為Address selfdestruct(address recipient): destroy the current contract, sending its funds to the given Address 銷燬當前合約,傳送當前以太坊餘額到給定的地址 suicide(address recipient): selfdestruct的別名函式

#5. 區塊和交易屬性 block.blockhash(uint blockNumber) returns (bytes32): 給定區塊的雜湊—僅對最近的 256 個區塊有效而不包括當前區塊 block.coinbase (address): 挖出當前區塊的礦工地址 block.difficulty (uint): 當前區塊難度 block.gaslimit (uint): 當前區塊 gas 限額 block.number (uint): 當前區塊號 block.timestamp (uint): 自 unix epoch 起始當前區塊以秒計的時間戳 msg.data (bytes): 完整的 calldata msg.gas (uint): 剩餘 gas msg.sender (address): 訊息傳送者(當前呼叫) msg.sig (bytes4): calldata 的前 4 位元組(也就是函式識別符號) msg.value (uint): 隨訊息傳送的 wei 的數量 now (uint): 目前區塊時間戳(block.timestamp) tx.gasprice (uint): 交易的 gas 價格 tx.origin (address): 交易發起者(完全的呼叫鏈)

註解 對於每一個外部函式呼叫,包括 msg.sender 和 msg.value 在內所有 msg 成員的值都會變化。這裡包括對庫函式的呼叫。

註解 不要依賴 block.timestamp、 now 和 block.blockhash 產生隨機數,除非你知道自己在做什麼。 時間戳和區塊雜湊在一定程度上都可能受到挖礦礦工影響。例如,挖礦社群中的惡意礦工可以用某個給定的雜湊來執行賭場合約的 payout 函式,而如果他們沒收到錢,還可以用一個不同的雜湊重新嘗試。 當前區塊的時間戳必須嚴格大於最後一個區塊的時間戳,但這裡唯一能確保的只是它會是在權威鏈上的兩個連續區塊的時間戳之間的數值。

註解 基於可擴充套件因素,區塊雜湊不是對所有區塊都有效。你僅僅可以訪問最近 256 個區塊的雜湊,其餘的雜湊均為零。

#6. 錯誤處理 assert(bool condition): 如果條件不滿足就丟擲—用於內部錯誤。

require(bool condition): 如果條件不滿足就拋掉—用於輸入或者外部元件引起的錯誤。

revert(): 終止執行並恢復狀態變動。

7 數學和密碼學函式

addmod(uint x, uint y, uint k) returns (uint): 計算 (x + y) % k,加法會在任意精度下執行,並且加法的結果即使超過 2**256 也不會被擷取。從 0.5.0 版本的編譯器開始會加入對 k != 0 的校驗(assert)。

mulmod(uint x, uint y, uint k) returns (uint): 計算 (x * y) % k,乘法會在任意精度下執行,並且乘法的結果即使超過 2**256 也不會被擷取。從 0.5.0 版本的編譯器開始會加入對 k != 0 的校驗(assert)。

keccak256(...) returns (bytes32): 計算 (tightly packed) arguments 的 Ethereum-SHA-3 (Keccak-256)雜湊。

sha256(...) returns (bytes32): 計算 (tightly packed) arguments 的 SHA-256 雜湊。

sha3(...) returns (bytes32): 等價於 keccak256。

ripemd160(...) returns (bytes20): 計算 (tightly packed) arguments 的 RIPEMD-160 雜湊。

ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address) : 利用橢圓曲線簽名恢復與公鑰相關的地址,錯誤返回零值。(example usage)

上文中的“tightly packed”是指不會對引數值進行 padding 處理(就是說所有引數值的位元組碼是連續存放的,譯者注),這意味著下邊這些呼叫都是等價的: keccak256("ab", "c") keccak256("abc") keccak256(0x616263) keccak256(6382179) keccak256(97, 98, 99) 如果需要 padding,可以使用顯式型別轉換:keccak256("\x00\x12") 和 keccak256(uint16(0x12)) 是一樣的。

請注意,常量值會使用儲存它們所需要的最少位元組數進行打包。例如:keccak256(0) == keccak256(uint8(0)),keccak256(0x12345678) == keccak256(uint32(0x12345678))。

在一個私鏈上,你很有可能碰到由於 sha256、ripemd160 或者 ecrecover 引起的 Out-of-Gas。這個原因就是他們被當做所謂的預編譯合約而執行,並且在第一次收到訊息後這些合約才真正存在(儘管合約程式碼是硬程式碼)。傳送到不存在的合約的訊息非常昂貴,所以實際的執行會導致 Out-of-Gas 錯誤。在你的合約中實際使用它們之前,給每個合約傳送一點兒以太幣,比如 1 Wei。這在官方網路或測試網路上不是問題。

8 Using for 如何使用

using A for B,這裡A通常是某個library裡面定義的某個方法,B是某種資料型別,這句話是把A方法繫結到B型別上,相當於給B型別附加了一個A方法。(也有翻譯為附著庫的) 在上面的例子中,將LibContract裡定義的方法繫結到所有的資料型別。但是一般我們不會在所有的型別例項上都去呼叫LibContract的方法,應該是要按需using的,這裡偷懶就寫*。 在通俗一點的例子就是, 比如 using LibInt for uint,然後LibInt裡面有定義一個toString方法。我們有一個uint a;那麼可以這樣呼叫a.toString(),toString方法在定義的時候,第一個引數會是一個uint型別的變數,表示呼叫者。

using A for B,A的函式的第一個引數必須和B的資料型別一致。
複製程式碼

還有這個方法是可以過載的,你可以定義好幾個同名的方法,但是第一個引數的型別不同,呼叫的時候自動的根據呼叫型別選擇某一種方法。

#9 陣列

陣列可以在宣告時指定長度,也可以動態調整大小。 對於 "儲存"的陣列來說,元素型別可以是任意的(即元素也可以是陣列型別,對映型別或者結構體)。 對於 "memory"的陣列來說,元素型別不能是對映型別,如果作為 public 函式的引數,它只能是 ABI 型別。

一個元素型別為 T,固定長度為 k 的陣列可以宣告為 T[k],而動態陣列宣告為 T[]。 舉個例子,一個長度為 5,元素型別為 uint 的動態陣列的陣列,應宣告為 uint[][5] (注意這裡跟其它語言比,陣列長度的宣告位置是反的)。 要訪問第三個動態陣列的第二個元素,你應該使用 x[2][1](陣列下標是從 0 開始的,且訪問陣列時的下標順序與宣告時相反,也就是說,x[2] 是從右邊減少了一級)。

bytesstring 型別的變數是特殊的陣列。 bytes 類似於 byte[],但它在 calldata 中會被“緊打包”(譯者注:將元素連續地存在一起,不會按每 32 位元組一單元的方式來存放)。 stringbytes 相同,但(暫時)不允許用長度或索引來訪問。

註解 如果想要訪問以位元組表示的字串 s,請使用 bytes(s).length / bytes(s)[7] = 'x';。 注意這時你訪問的是 UTF-8 形式的低階 bytes 型別,而不是單個的字元。

可以將陣列標識為 public,從而讓 Solidity 建立一個 getter。 之後必須使用數字下標作為引數來訪問 getter。

建立記憶體陣列

可使用 new 關鍵字在記憶體中建立變長陣列。 與"儲存"(storage)陣列相反的是,你不能通過修改成員變數 .length 改變 "記憶體"(memory)陣列的大小。

contract C {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        // 這裡我們有 a.length == 7 以及 b.length == len
        a[6] = 8;
    }
}
複製程式碼

陣列字面常數 / 內聯陣列

陣列字面常數是寫作表示式形式的陣列,並且不會立即賦值給變數。

contract C {
    function f() public pure {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] _data) public pure {
        // ...
    }
}
複製程式碼

陣列字面常數是一種定長的 "記憶體"(memory) 陣列型別,它的基礎型別由其中元素的普通型別決定。 例如,[1, 2, 3] 的型別是 uint8[3] memory,因為其中的每個字面常數的型別都是 uint8。 正因為如此,有必要將上面這個例子中的第一個元素轉換成 uint 型別。 目前需要注意的是,定長的 "記憶體"(memory) 陣列並不能賦值給變長的 "記憶體"(memory) 陣列,下面是個反例:

// 這段程式碼並不能編譯。

pragma solidity ^0.4.0;

contract C {
    function f() public {
        // 這一行引發了一個型別錯誤,因為 unint[3] memory
        // 不能轉換成 uint[] memory。
        uint[] x = [uint(1), 3, 4];
    }
}
複製程式碼

已經計劃在未來移除這樣的限制,但目前陣列在 ABI 中傳遞的問題造成了一些麻煩。

成員

length: 陣列有 length 成員變數表示當前陣列的長度。 動態陣列可以在 儲存storage (而不是 記憶體memory )中通過改變成員變數 .length 改變陣列大小。 並不能通過訪問超出當前陣列長度的方式實現自動擴充套件陣列的長度。 一經建立,記憶體memory 陣列的大小就是固定的(但卻是動態的,也就是說,它依賴於執行時的引數)。

push: 變長的 儲存storage 陣列以及 bytes 型別(而不是 string 型別)都有一個叫做 push 的成員函式,它用來附加新的元素到陣列末尾。 這個函式將返回新的陣列長度。

警告 在外部函式中目前還不能使用多維陣列。

警告 由於 以太坊虛擬機器Ethereum Virtual Machine(EVM) 的限制,不能通過外部函式呼叫返回動態的內容。 例如,如果通過 web3.js 呼叫 contract C { function f() returns (uint[]) { ... } } 中的 f 函式,它會返回一些內容,但通過 Solidity 不可以。 目前唯一的變通方法是使用大型的靜態陣列。

contract ArrayContract {
    uint[2**20] m_aLotOfIntegers;
    // 注意下面的程式碼並不是一對動態陣列,
    // 而是一個陣列元素為一對變數的動態陣列(也就是陣列元素為長度為 2 的定長陣列的動態陣列)。
    bool[2][] m_pairsOfFlags;
    // newPairs 儲存在 memory 中 —— 函式引數預設的儲存位置

    function setAllFlagPairs(bool[2][] newPairs) public {
        // 向一個 storage 的陣列賦值會替代整個陣列
        m_pairsOfFlags = newPairs;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // 訪問一個不存在的陣列下標會引發一個異常
        m_pairsOfFlags[index][0] = flagA;
        m_pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) public {
        // 如果 newSize 更小,那麼超出的元素會被清除
        m_pairsOfFlags.length = newSize;
    }

    function clear() public {
        // 這些程式碼會將陣列全部清空
        delete m_pairsOfFlags;
        delete m_aLotOfIntegers;
        // 這裡也是實現同樣的功能
        m_pairsOfFlags.length = 0;
    }

    bytes m_byteData;

    function byteArrays(bytes data) public {
        // 位元組的陣列(語言意義中的 byte 的複數 ``bytes``)不一樣,因為它們不是填充式儲存的,
        // 但可以當作和 "uint8[]" 一樣對待
        m_byteData = data;
        m_byteData.length += 7;
        m_byteData[3] = byte(8);
        delete m_byteData[2];
    }

    function addFlag(bool[2] flag) public returns (uint) {
        return m_pairsOfFlags.push(flag);
    }

    function createMemoryArray(uint size) public pure returns (bytes) {
        // 使用 `new` 建立動態 memory 陣列:
        uint[2][] memory arrayOfPairs = new uint[2][](size);
        // 建立一個動態位元組陣列:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = byte(i);
        return b;
    }
}
複製程式碼

18. solidity常見錯誤提示及原因分析

1). 智慧合約執行失敗

告警描述: " Warning! Error encountered during contract execution [Out of gas] " 發生場景: 執行官網眾籌智慧合約程式碼,在給智慧合約打代幣前,往智慧合約地址賬戶打ETH,交易失敗,提示如下: 點選檢視資訊連結 程式碼及原因分析: 下面是執行的回撥函式,其中“tokenReward.transfer(msg.sender, amount / price);”的意思就是把智慧合約的代幣打給傳送者賬號。這個賬號還沒有代幣,所以肯定會執行失敗。

    function () payable {
        require(!crowdsaleClosed);
        uint amount = msg.value;
        balanceOf[msg.sender] += amount;
        amountRaised += amount;
        tokenReward.transfer(msg.sender, amount / price);
        FundTransfer(msg.sender, amount, true);
    }
複製程式碼

解決方法: 先往智慧合約賬號打代幣,然後打ETH,就不會執行失敗了。

####2). REMIX+MetaMASK的場景下,呼叫智慧合約函式,提示不可知地址 告警描述: REMIX輸出框輸出以下告警內容: transact to Crowdsale.checkGoalReached errored: Unknown address - unable to sign transaction for this address: "0x3d7dfb80e71096f2c4ee63c42c4d849f2cbbe363" 發生場景: 更換了Meta賬號後,呼叫之前賬號建立的智慧合約的函式 原因分析: Remix輸出框提示未知地址:

告警描述
檢視MetaMask的當前賬號,發現是Account 1的賬號,所以無法執行合約。
MetaMask的當前賬號
解決方法: 更換MetaMask為Account8(地址為0x3D7DfB80E71096F2c4Ee63C42C4D849F2CBBE363)的賬號即可正常執行。

3). GETH安裝時出現異常

告警描述: GETH安裝時出現以下告警: E: Failed to fetch http://101.110.118.22/ppa.launchpad.net/ethereum/ethereum/ubuntu/pool/main/e/ethereum/ethereum_1.8.10+build13740+artful_i386.deb Could not connect to 101.110.118.22:80 (101.110.118.22), connection timed out E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing? 發生場景: GETH安裝,執行以下命令出現。

sudo apt-get install ethereum

原因分析: 解決方法: <1> 參考網上解決方案 sudo vim /etc/resolv.conf ,新增:

nameserver 8.8.8.8

然後執行:

sudo /etc/init.d/networking restart

還是不行。 <2> 從綠地金融中心搬到家裡,做相同的操作,就可以安裝成功了。 應該是綠地金融中心的路由器做了某些配置,導致下載不成功。

4). 問題描述模板

告警描述:

DocstringParsingError: Documented parameter "owner" not found in the parameter list of the function.
複製程式碼

發生場景: remix編譯以下智慧合約函式時出現:

    /** 
     * @dev Withdraw the token remained to the constructor address.
     * @param owner Address of owner.
     */
    function withdrawToken() public onlyCreator{
        if( 0 < token.balanceOf(address(this))) {
           token.transfer(creator, token.balanceOf(address(this)));
        }
    }    

複製程式碼

原因分析: remix竟然要檢測@param後面跟的引數,看來註釋不能隨便亂寫了。 解決方法: 函式修改為以下的就能編譯通過了。

    /** 
     * @dev Withdraw the token remained to the constructor address.
     */
    function withdrawToken() public onlyCreator{
        if( 0 < token.balanceOf(address(this))) {
           token.transfer(creator, token.balanceOf(address(this)));
        }
    } 
複製程式碼

2). 問題描述模板

告警描述: 發生場景: 原因分析: 解決方法:

9. 常見問題及解答

1).modifer函式是幹什麼的?

2).如何打幣回支付賬號?

3).智慧合約的定時器和系統函式是什麼?

4).當建立一個智慧合約時,msg.sender和this的區別? 答覆:msg.sender是指外部賬戶的地址,this是指當前建立的智慧合約的地址。

contract AddrTest{
    event LogContractAddress(address exAccount, address contractAddress);
    
    function AddrTest(){
    LogContractAddress(msg.sender,this);
    }    
}
複製程式碼

在remix執行,可以證明這個推測

LOG說明

10 View Functions,Pure Functions,Fallback Function的定義?

答案:點選參考官網文件,還沒有時間翻譯過來。

11. 文件參考

1,官方中文網站 http://solidity-cn.readthedocs.io/zh/develop/

2, tiny熊翻譯系列 1] Solidity教程式列1 - 型別介紹 2] 智慧合約語言Solidity教程系列2 - 地址型別介紹 3] 智慧合約語言 Solidity 教程系列3 - 函式型別 4] 智慧合約語言 Solidity 教程系列4 - 資料儲存位置分析 5] 智慧合約語言 Solidity 教程系列5 - 陣列介紹 6] 智慧合約語言 Solidity 教程系列6 - 結構體與對映 7] 智慧合約語言 Solidity 教程系列7 - 以太單位及時間單位 8] 智慧合約語言 Solidity 教程系列8 - Solidity API 9] 智慧合約語言 Solidity 教程系列9 - 錯誤處理 10] 智慧合約語言 Solidity 教程系列10 - 完全理解函式修改器

相關文章