iT邦幫忙

2024 iThome 鐵人賽

DAY 15
0
自我挑戰組

Solidity 初學之路系列 第 15

DAY 15 - 接收與發送 ETH

  • 分享至 

  • xImage
  •  

在寫接收 ETH 合約時,會用到有兩個特殊的函數:receive()fallback()
寫發送合約則可以用 transfer()send()call() 三種方法。

接收 ETH

Solidity 支援兩種特殊的回退函數,receive()fallback(),他們主要在兩種情況下被使用:

  1. 接收ETH
  2. 處理合約中不存在的函數呼叫(代理合約 proxy contract)

註:Solidity 0.6.x 版本之前,語法上只有 fallback() 函數,用來接收用戶發送的 ETH 時調用以及在被調用函數簽名沒有匹配到時來調用。 0.6 版本之後,Solidity 才將 fallback() 函式拆分成 receive() 和 fallback() 兩個函式。

接收 ETH 函數 receive

receive() 函數是在合約收到 ETH 轉帳時被呼叫的函數。一個合約最多有一個receive() 函數,宣告方式與一般函數不一樣,不需要 function 關鍵字,語法:

receive() external payable { ... }

receive() 函數不能有任何的參數,不能傳回任何值,必須包含 externalpayable
當合約接收 ETH 的時候,receive() 會被觸發。 receive() 最好不要執行太多的邏輯,因為如果別人用 sendtransfer 方法發送 ETH 的話,gas 會限制在 2300,receive() 太複雜可能會觸發 Out of Gas 報錯;如果用 call 就可以自訂 gas 執行更複雜的邏輯(這三種發送ETH的方法之後會提)。
receive() 裡發送一個 event

// 定義事件
event Received(address Sender, uint Value);
// 接收 ETH 時釋放 Received 事件
receive() external payable {
    emit Received(msg.sender, msg.value);
}

有些惡意合約,會在 receive() 函數(舊版就是 fallback() 函數)嵌入惡意消耗 gas 的內容或使得執行故意失敗的程式碼,導致一些包含退款和轉帳邏輯的合約不能正常運作,因此寫包含退款等邏輯的合約時候,一定要注意這種情況。

回退函數 fallback

fallback() 函數會在呼叫合約不存在的函數時被觸發。可用於接收 ETH,也可以用於代理合約 proxy contract。fallback() 聲明時不需要 function 關鍵字,必須由 external 修飾,一般也會用 payable 修飾,用來接收 ETH,語法:

fallback() external payable { ... }

定義一個 fallback() 函數,被觸發時候會釋放 fallbackCalled 事件,並輸出 msg.sendermsg.valuemsg.data:

event fallbackCalled(address Sender, uint Value, bytes Data);

// fallback
fallback() external payable{
    emit fallbackCalled(msg.sender, msg.value, msg.data);
}

receivefallback 的差別

receive 和 fallback 都能夠用來接收 ETH,他們觸發的規則如下:

觸發fallback() 還是 receive()?
           接收ETH
              |
        msg.data是空?
             /  \
           是    否
          /        \
 receive()存在?    fallback()
       /  \
     是    否
     /      \
receive()  fallback()

簡單來說,合約接收 ETH 時,msg.data 為空且存在 receive() 時,會觸發 receive()msg.data 不為空或不存在 receive() 時,會觸發 fallback(),此時 fallback() 必須為 payablereceive()payable fallback() 皆不存在的時候,向合約直接發送 ETH 將會報錯(你仍可以透過帶有 payable 的函數向合約發送 ETH)。

接收 ETH 合約

我們先部署一個接收 ETH 合約 ReceiveETHReceiveETH 合約裡有一個事件 Log,記錄收到的 ETH 數量和 gas 剩餘。還有兩個函數,一個是 receive() 函數,收到 ETH 被觸發,並且傳送 Log 事件;另一個是查詢合約 ETH 餘額的 getBalance() 函數。

contract ReceiveETH {
    // 收到 ETH 事件,記錄 amount 和 gas
    event Log(uint amount, uint gas);
    
    // receive 方法,接收 eth 時被觸發
    receive() external payable{
        emit Log(msg.value, gasleft());
    }
    
    // 返回合約 ETH 餘額
    function getBalance() view public returns(uint) {
        return address(this).balance;
    }
}

發送 ETH

Solidity 有三種方法向其他合約發送 ETH:transfer()send()call(),其中 call() 是比較好的作法。
首先定義一個 SendETH 合約,實現 payable 的建構子和 receive,讓我們在部署時和部署後可以向 ReceiveETH 合約發送ETH。

contract SendETH {
    // 建構函數,payable 使得部署的時候可以轉 ETH 進去
    constructor() payable{}
    // receive 方法,接收 ETH 時被觸發
    receive() external payable{}
}

1. transfer 函數

    <接收方位址>.transfer(<發送ETH金額>)
  • transfer() 的 gas 限制是 2300,足夠用於轉賬,但對方合約的 fallback() receive() 函數不能實現太複雜的邏輯。
  • transfer() 如果轉帳失敗,會自動 revert(回滾交易)。
  • 範例:_toReceiveETH 合約的地址,amount 是 ETH 轉帳金額
    // 用 transfer() 發送 ETH
    function tranferETH(address payable _to, uint256 amount) external payable{
        _to.transfer(amount);
    }
    

2. send 函數

<接收方位址>.send(<發送ETH金額>)
  • send() 的 gas 限制是 2300,足夠用於轉賬,但對方合約的 fallback()receive() 函數不能實現太複雜的邏輯。
  • send() 如果轉帳失敗,不會 revert。
  • send() 的回傳值是 bool,代表轉帳成功或失敗,需要額外程式碼處理一下。
  • 範例:
    error SendFailed(); // 用 send 發送 ETH 失敗 error
    
    // send() 發送 ETH
    function sendETH(address payable _to, uint256 amount) external payable{
        // 處理下 send 的回傳值,如果失敗,revert 交易並傳送 error
        bool success = _to.send(amount);
        if(!success){
            revert SendFailed();
        }
    }
    

3. call 函數

<接收方位址>.call{value: <發送ETH金額>}("")
  • call() 沒有 gas 限制,可以支援對方合約 fallback()receive() 函數實現複雜邏輯。
  • call() 如果轉帳失敗,不會 revert。
  • call() 的回傳值是 (bool, bytes),其中 bool 代表著轉帳成功或失敗,需要額外程式碼處理一下。
  • 範例:
    error CallFailed(); // // 使用 call 發送 ETH 失敗 error
    
    // call() 發送 ETH
    function callETH(address payable _to, uint256 amount) external payable{
        // 處理下 call 的回傳值,如果失敗,revert 交易並發送 error
        (bool success,) = _to.call{value: amount}("");
        if(!success){
            revert CallFailed();
        }
    }
    

發送 ETH 函數比較

  • call 沒有 gas 限制,最靈活,是最提倡的方法
  • transfer 有 2300 gas 限制,但發送失敗會自動 revert 交易,是次優選擇
  • send 有 2300 gas 限制,而且發送失敗不會自動 revert 交易,幾乎沒有人使用它

上一篇
DAY 14 - 函數重載、庫合約
下一篇
DAY 16 - 調用其他合約、Call
系列文
Solidity 初學之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言