本篇預計閱讀時間 20 分鐘, 實作時間 50 分鐘。

在開始之前先來思考底下的練習題,閱讀後即可練習完成。

練習題

  • 練習題 1

    • 請敘述四種 Solidity 函式的可視性分別對於當前合約 (MyContract)、繼承合約 (DerivedContract)、其他合約 (AnotherContract)的訪問權限。

  • 練習題 2

    • 請實作一函式 getContractBalance() 來查看當前合約擁有的餘額。

  • 練習題 3

    • 請使用 Fallback Function 使合約可以接收外部的匯款。 並且使用 REMIX 介面匯款給合約。最後利用上一題的 getContractBalance() 查看一切是否運作正常。

 

Function 又被翻譯為函數、方法、函式等,藉由呼叫(Call)以及 輸入參數(也可不輸入)後來達到某些行為,像是回傳(Returns,也可不回傳)或修改某些值。 當我們在一個函數的 parameter 前宣告 constant 時,此變數就不允許被賦值(除了初始化),也不佔用存儲槽。

函式分類

呼叫函式通常有幾種情況,一種為 Writing Transactions,也就是對區塊鏈上的狀態進行修改,這時我們呼叫函式這個動作也會成為一筆交易被記錄在鏈上。另一種為 Reading Transactions,為閱讀鏈上的資料(閱讀鏈上交易),不會修改到鏈上的狀態,因此也是免費的。

Returns

我們可以在定義函數時的參數後加上 returns 來敘述回傳值的型態。

以下的例子我們要回傳一個布林值,所以就在 returns 後敘述 bool

pragma solidity ^0.8.11;
contract MyContract 
{function myFunction() public view returns (bool) {
    return true;
    }
}

Solidity 和其他強型別語言不同的是,他可以回傳多個值,也就是 Multiple Return Values 的使用:


pragma solidity ^0.8.11;
contract MyContract {
    uint public x;
    uint public y;
    uint public z;
    function multipleReturns() public view returns(uint a, uint b, uint c) {
        return (1, 2, 3);
    }

    function processMultipleReturns() external {

    // 這邊我們可以一次賦值多個內容,是非常好用的寫法。

    (x, y, z) = multipleReturns();

}

    function getLastReturnValue() external {

    // 如果我們只想要把 multipleReturns() 這個函數回傳的第三個值賦值給 c ,那我們可以利用以下方法

    (,,z) = multipleReturns();

    }

}

Visibility

首先我們要知道可以調用一個「函數」的角色有三者,分別為:

  • 當前合約 (MyContract)

  • 繼承合約 (DerivedContract)

  • 其他合約 (AnotherContract)

當前合約與繼承合約兩者合稱內部(internal),而其他合約不屬於我們這個「家族」因此被稱為外部(external)。

在 Solidity 中可視性可以定義此函數執行或訪問上的權限。

contract MyContract {
function myFunction () [visibility-here] {
// do something
    }
}
  • public - 任何的合約和帳戶都可以呼叫這個函數,也就是在合約的外部和內部均可見。

  • private - 只有當前合約可以呼叫這個函數,任何外部或繼承合約皆不可呼叫。

  • external - 只有除了自己和繼承合約以外的合約或帳戶可以呼叫此函數。

  • internal - 只有自己和繼承合約的合約或帳戶可以呼叫此函數。

如果真的不知道一個函數的可視性該是什麼,把一個函數設為 private 是一個好習慣,在測試的時候再根據使用情況回去把程式碼裡面的函數改成我們要的權限。

但需要注意的是所有在合約內的東西對外部觀察者來說都是可見的,我們設定可視性要避免的是其他合約對我們的狀態進行修改和訪問,但相關訊息是無法隱藏的。

stateMutability

除了以上的可視性之外,我們還可以增加一些函數的敘述來定義這個函數的類型。

function myFunction()
    <visibility specifier>
    <stateMutability>
    returns (bool) {
    return true;
}
  • pure : 不允許修改或訪問狀態變量

  • view : 不允許修改狀態變量

  • payable : 允許函數在呼叫同時接收由 msg.value 或者其他匯款函式傳來的以太幣,只有宣告 payable 的物件才可以進行交易

View vs. Pure

  • 在早些版本他們被稱為 constant 函數

  • View Function: 可閱讀但不可更改任何狀態變數

  • Pure Function: 不可閱讀也不可更改任何狀態變數

Pure Function

  • 可以呼叫其他的 Pure function

  • 不可以呼叫 View function,也不可以呼叫其他的沒有標明 Pure 或 View,但卻會更改到狀態的函數(也被稱作 Writing function )

  • 不可以讀取或對狀態變數做任何更改

View Function

  • 可以存取任何的狀態變數,但僅能做讀取用,不可進行修改。

  • 可以呼叫其他的 Pure function

  • 不可以呼叫其他的沒有標明 pure view,會更改到狀態的函數(也被稱作Writing function)

Constructor

建構子(constructor)是一個在合約被創建時只會跑一次的函數,像是佈署到鏈上時,可用於初始化或宣告某些我們希望一開始就執行的變數。

contract myContract {
    uint a;
    constructor() public {
         a = 0;
    }
}

關於建構子有以下規定:

  1. 一個 Contract 只能有一個建構子。

  2. 可視性可以為 publicinternal

  3. 如果一個 Contract 被標示為 internal,則其為 Abstract Contract

  4. constructor 也可以像普通函數一樣有參數。

我們也可以在建構子裡面傳入參數。

contract myContract {
    uint a;
    constructor(uint new_a) public {
        a = new_a;
    }
}

Function Overloading

在同一樣一個作用域中我們可對同樣(名稱、可視性、回傳值相同)的函數 進行 Function Overloading,此時根據我們輸入的參數數量,或者是輸入的參數型態不同,Solidity 會自動去尋找符合的函數執行。

pragma solidity ^0.8.11;

contract Test {
    function getSum(uint a, uint b) public pure returns(uint){
return a + b;
}
    function getSum(uint a, uint b, uint c) public pure returns(uint){
    return a + b + c;
}
    function callSumWithTwoArguments() public pure returns(uint){
    return getSum(1,2);
}
    function callSumWithThreeArguments() public pure returns(uint){
    return getSum(1,2,3);
    }
}

Fallback

Fallback 是一個沒有名字、沒有參數也不會回傳任何值的函數,通常在呼叫合約或者是對合約匯款時,沒有與之匹配的函式便會觸發 Fallback Function。

他在以下情況會被執行:

  • 當一筆非由呼叫函式引起的交易被送往合約時

  • 當呼叫的函式不存在且 fallback 函式存在時

  • 合約直接匯出 ether 但是 receive() 不存在或 msg.data 非為空時

若是合約接收到了 Ether 卻沒有任何的 Fallback Function 也沒有任何呼叫函式的行為,將會 Throw Exception,並退還 Ether。

Fallback 有 2300 gas 限制,也就是說有 Fallback Function 的存在可以保證函數執行的花費控制在 2300 gas 以內。同時我們在佈署合約之前也必須確實的測試 Fallback Function 以把函式的執行花費控制住。

pragma solidity ^0.8.10;

contract Fallback {
    event Log(uint gas);
    // Fallback function must be declared as external.
    // Fallback function 必須是 external 的

    fallback() external payable {
    // send / transfer (forwards 2300 gas to this fallback function)
   // call (forwards all of the gas)
   emit Log(gasleft());
  }

    // 我們使用 getBalance 來查看當前合約的餘額
    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint) {return address(this).balance;
    }
}

contract SendToFallback {
    function transferToFallback(address payable _to) public payable {_to.transfer(msg.value);
}
function callFallback(address payable _to) public payable {
(bool sent, ) = _to.call{value: msg.value}("");
require(sent, "Failed to send Ether");
  }
}

Fallback Function 有以下的特性:

  • 可視性只能為 external

  • 當我們沒有任何 payable 的 Function 符合調用,那就會觸發例外處理(exception),除非我們擁有 Fallback function

  • Fallback Function 就像 catch,當我們沒有和任何 payable function互動,或沒有任何函式符合交易的 encoded data field,就會觸發。

pragma solidity ^0.8.11;

contract FunctionsExample{
    mapping(address => uint) public balanceReceived;
    address payable owner;
    constructor() public {
        owner = payable(msg.sender);
    }
    function getOwner () public view returns(address){
      return owner;
    }
    function convertWeiToEther(uint _amountInWei) public pure returns(uint){
    return _amountInWei / 1 ether;
    //我們也可以使用以下程式碼做到同樣功效
    // return _amountInWei / 10**18;
    // pure function call 只可以跟區域變數, 像是 _amountInWei 互動
    // 不可以是在這個函式(作用域, scope)以外的 state variable
    // pure function call only interact with the variables in this scope like _amountInWei, but not the state variables outside the scope

}

    function destroyContract() public {
      require(msg.sender == owner, "You are not the owner");
      selfdestruct(owner);
}

    function receiveMoney() public payable {
assert(balanceReceived[msg.sender] + msg.value >= balanceReceived[msg.sender]);
balanceReceived[msg.sender] += msg.value;
}

    function withdrawMoney(address payable _to, uint _amount) public {

        require(balanceReceived[msg.sender] >= _amount, "You don't have enough ether");
        assert(balanceReceived[msg.sender] >= balanceReceived[msg.sender] - _amount);

        balanceReceived[msg.sender] -= _amount;
        _to.transfer(_amount);
}

        fallback () external payable{
            receiveMoney();
    // fallback function 在 REMIX IDE 可以利用輸入格來互動, 即便我們沒有在這裡宣告任何參數
   // fallback function will have input fill(in remix IDE) even we didn't declare any input arguments in function
   // because the fallback function is triggered automatically no matter have arguments or not.
   // arguments data is in msg.data}}

Fallback Function 存在最大的原因是我們沒辦法避免接收 ether,常見情況下我們有至少三種的方式可以接收來自外部的 ether:

  1. 當我們呼叫了 selfdistructor 解構其他合約並把自己的合約地址當作參數傳入

  2. 挖礦,我們將智能合約的地址設為礦工地址

  3. 在智能合約被部屬之前就先將 ether 傳至其地址(機率不高但有可能發生)

最糟的情況下,我們還可以依賴 2300 gas 的限制:

  • _contractAddress.trasfer(1 ether);

當 Contract Data 被呼叫時使其強制地避免函式執行

  • require(msg.data.length == 0)

練習題解答

  • 練習題 1

    • 請敘述四種 Solidity 函式的可視性分別對於當前合約 (MyContract)、繼承合約 (DerivedContract)、其他合約 (AnotherContract)的訪問權限。

public - 任何的合約和帳戶都可以呼叫這個函數,也就是在合約的外部和 內部均可見。

private - 只有當前合約可以呼叫這個函數,任何外部或繼承合約皆不可 呼叫。

external - 只有除了自己和繼承合約以外的合約或帳戶可以呼叫此函數。

internal - 只有自己和繼承合約的合約或帳戶可以呼叫此函數。

  • 練習題 2

    • 請實作一函式 getContractBalance() 來查看當前合約擁有的餘額。

本題提供兩個方法:

方法 1:

// Method 1

pragma solidity ^0.8.11; contract Practice{

function getContractBalance() public returns(uint){ return address(this).balance;

    } 
}

使用方法 1 可能會無法在 REMIX 的函式互動介面找到我們要的答案。

我們可以去 Console Window 找到我們剛剛與函式互動交易的結果來查看。

圖 5-1

圖 5-1 中,可以發現在 decoded output 中有我們想要的 balance { "0": "uint256: 0" },如果我們想要用其他的方式知道函數的運作結果,可以使用之後章節會提到的「事件(event)」來操作。

方法 2:

// Method 2

pragma solidity ^0.8.11; 
contract Practice{

    uint public bal = 0;
    function getContractBalance() public returns(uint){
        bal = address(this).balance;
        return bal; 
    }
}

為什麼我們沒辦法直接在 Remix 的合約互動介面中直接看見? 

因為我們沒有把函式的可變性定義為 view。也就是說如果我們沒有把唯讀這個修飾詞補上,在編譯後就會將其視為 writing function,回傳結果也不會在互動介面上出現了。

這邊需要注意的是我們沒有辦法直接寫 return address(this).balance

因為編譯器並不知道我們現在的 this 是什麼,必須要等到合約交易之後的 address 出現才可利用外部函式的方式去呼叫。

所以修正方法是用一個變數先把它存起來!

pragma solidity ^0.8.11; contract Practice{

function getContractBalance() public view returns(uint){ uint now = address(this).balance;
return now;

    }

fallback() external payable{
    } 
}
  • 練習題 3

    • 請使用 Fallback Function 使合約可以接收外部的匯款。 並且使用 REMIX 介面匯款給合約。最後利用上一題的 getContractBalance() 查看一切是否運作正常。

實作完以上合約,經過編譯與佈署之後我們可以透過互動介面裡面的 Value 選項,

並搭配最下方 Low-Level Interactions 的 transaction 來實現匯款給合約,並以 Fallback Function 接收的目的。

圖 5-2

如果我們是用方法一來寫第 2 題的話,我們也可以在呼叫 getContract- Balance() 交易結果中察看是否正確,如果不是的話就可以用 Remix 提供的互動介面來查看回傳結果。

圖 5-3