--- title: 'Solidity WTF 102 17 ~ 21 單元' lang: zh-tw --- Solidity WTF 102 17 ~ 21 單元 === :::info :date: 2024/10/02 ::: [TOC] # 課程學習 ## 庫合約 目的是提升程式碼的複用性和減少`gas`而存在。 有以下規則: 1. 不能存在状态变量 1. 不能够继承或被继承 1. 不能接收以太币 1. 不可以被销毁 使用法方如下: ```xml= <!-- 這邊代表會Strings庫會自動添加uint256,意思就是可以直接把聲明為uint256的變數,接上Strings庫裡的function名稱使用。 --> using Strings for uint256; <!-- function中使用到uint256聲明 --> function getString1(uint256 _number) public pure returns(string memory){ return _number.toHexString(); } ``` ```xml= <!-- 也可以直接調用庫名稱 --> function getString2(uint256 _number) public pure returns(string memory){ return Strings.toHexString(_number); } ``` :::info :bulb:庫合約重點是能夠著重在重複性上,並且使用安全係數較告的庫合約,能夠有效減少自行開發時間。 ::: ## Import 下列是`import`方法: ```xml= <!-- 直接使用相對路徑,又稱全局導入 --> import './Yeye.sol'; <!-- 使用網址import --> import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol'; <!-- 使用npm目錄導入 --> import '@openzeppelin/contracts/access/Ownable.sol'; <!-- 這種方法叫做命名導入,他可以從引入的合約中import自己想要的合約 --> import {Yeye} from './Yeye.sol'; <!-- 別名導入YeyeTest是我指定的名稱,那我就可以使用這名稱,來呼叫Yeye合約內部 --> import * as YeyeTest from './Yeye.sol' ``` ## 接收ETH 有兩種`特殊函數`,用來當我`接收ETH`時會`自動觸發`的函數,`receive()`和`fallback()`。 :::warning 0.6版本前,只有`fallback` ::: ### 接收方式 通常有可能使用`transfer`或是`send`來`發送ETH`,因為這兩者的`gas limit is 2300`,所以如果`receive or fallback`中函數寫得太過於複雜,則會導致噴錯,因為`gas`超過(`Out of Gas`)。 ```xml= event Received(address Sender, uint Value); event fallbackCalled(address Sender, uint Value, bytes Data); <!-- 下面是兩種特殊函數,接收ETH會自動觸發 --> receive() external payable { emit Received(msg.sender, msg.value); } // fallback fallback() external payable{ emit fallbackCalled(msg.sender, msg.value, msg.data); } ``` ### receive和fallback區別 這邊有一個樹狀圖,他會依照下列方式判別應該使用`receive()`和`fallback()` ```xml= 触发fallback() 还是 receive()? 接收ETH | msg.data是空? / \ 是 否 / \ receive()存在? fallback() / \ 是 否 / \ receive() fallback() ``` :::warning 這邊要注意,如果當下應該使用到的特殊函數若沒有實現(被寫出),否則會噴錯。 ::: ## 發送ETH 有三種發送ETH的方法,`transfer()`、`send()`、`call()`,其中`call()`是被鼓勵的方法,因為沒有`gas`限制,可以在此或是`接收ETH`特殊函數中,寫複雜邏輯也不會因為`out of gas`而噴錯。 ### 實現方法 > 個人認為,課程內容提到算滿完整,也沒有特別的坑,所以直接引入課程中的文案。 #### transfer * 用法是接收方地址`.transfer(发送ETH数额)`。 * `transfer()`的`gas`限制是`2300`,足够用于转账,但对方合约的`fallback()`或`receive()`函数不能实现太复杂的逻辑。 * `transfer()`如果转账失败,会自动`revert`(回滚交易)。 ```javascript= function transferETH(address payable _to, uint256 amount) external payable{ _to.transfer(amount); } ``` #### send * 用法是接收方地址`.send(发送ETH数额)`。 * `send()`的`gas`限制是`2300`,足够用于转账,但对方合约的`fallback()`或`receive()`函数不能实现太复杂的逻辑。 * `send()`如果转账失败,不会`revert`。 * `send()`的返回值是`bool`,代表着转账成功或失败,需要额外代码处理一下。 ```javascript= 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(); } } ``` #### call * 用法是接收方地址`.call{value: 发送ETH数额}("")`。 * `call()`没有`gas`限制,可以支持对方合约`fallback()`或`receive()`函数实现复杂逻辑。 * `call()`如果转账失败,不会`revert`。 * `call()`的返回值是`(bool, bytes)`,其中`bool`代表着转账成功或失败,需要额外代码处理一下。 ```javascript= 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(); } } ``` >這邊要注意`transfer`發送失敗會自動`revert`交易,`send`不會 :::info :bulb: 若要選擇使用順序,則會是`call` -> `transfer` -> `send` ::: ## 調用其他合約 ### 調用已部屬合約 假設已經完成一個合約如下 ```javascript= contract OtherContract { uint256 private _x = 0; // 状态变量_x // 收到eth的事件,记录amount和gas event Log(uint amount, uint gas); // 返回合约ETH余额 function getBalance() view public returns(uint) { return address(this).balance; } // 可以调整状态变量_x的函数,并且可以往合约转ETH (payable) function setX(uint256 x) external payable{ _x = x; // 如果转入ETH,则释放Log事件 if(msg.value > 0){ emit Log(msg.value, gasleft()); } } // 读取_x function getX() external view returns(uint x){ x = _x; } } ``` 我想要使用上面已部屬合約,那我可以這樣調用 ```javascript= // 传入合约地址 function callSetX(address _Address, uint256 x) external{ OtherContract(_Address).setX(x); } // 传入合约变量 function callGetX(OtherContract _Address) external view returns(uint x){ x = _Address.getX(); } // 创建合约变量 function callGetX2(address _Address) external view returns(uint x){ OtherContract oc = OtherContract(_Address); x = oc.getX(); } // 调用合约并发送ETH function setXTransferETH(address otherContract, uint256 x) payable external{ OtherContract(otherContract).setX{value: msg.value}(x); } ``` ## 重點 我認為這邊有一個重點是在調用合約的部分,我直接拿測試題目來說明,我認為當初自己有點不太明白。 ```javascript= 我有一個合約與接口如下 //OtherContract 合约如下: // SPDX-License-Identifier: MIT pragma solidity ^0.8.6; interface IOtherContract { function getBalance() external returns(uint); function setX(uint256 x) external payable; function getX() external view returns(uint x); } contract OtherContract is IOtherContract{ uint256 private _x = 0; event Log(uint amount, uint gas); function getBalance() external view override returns(uint) { return address(this).balance; } function setX(uint256 x) external override payable{ _x = x; if(msg.value > 0){ emit Log(msg.value, gasleft()); } } function getX() external view override returns(uint x){ x = _x; } } ``` 並且我在另一個合約想要調用,於是我這樣聲明 ```javascript= (1) OtherContract other = OtherContract(0xd9145CCE52D386f254917e481eB44e9943F39138) (2) IOtherContract other = IOtherContract(0xd9145CCE52D386f254917e481eB44e9943F39138) ``` 當下如果都部屬,其實會是正確的,但只部屬IOtherContract的情況下,(2)可能會是錯誤的,因為他沒有實現結果。