Solidity學習筆記-2

-WZM-發表於2024-11-30

16.函式過載

16_01.過載

函式過載(overloading):即函式名字相同,但輸入的引數型別不同的函式可以同時存在;(被視為是不同的函式)

Solidity不允許修飾器modifier過載;

過載的函式經過編譯之後,由於不同的引數型別,都變成了不同的函式選擇器(selector,29節有介紹);

示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract A{
    // 無傳入引數,輸出"No parameter"
    function saySomething() public pure returns (string memory) {
        return "No parameter";
    }
    // 傳入string,輸出string
    function saySomething(string memory str) public pure returns (string memory){
        return str;
    }
}
image-20241117163811702

16_02.實參匹配

呼叫過載函式時,會把輸入的實際資料函式引數的型別進行匹配,若出現多個匹配的過載函式,會報錯;

示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract A{
    uint256 data = 0;
    // 傳入引數是uint8
    function Add(uint8 num) public {
        data += num;
    }
    // 傳入引數是uint256
    function Add(uint256 num) public {
        data += num;
    }

    function callAdd() public pure  returns (string memory){
        // 50即可用是uint8,也可以是uint256
        // 因此編譯會報錯
        Add(50);
        return "call Add function sucess";
    }
}

單獨將Add兩個函式編譯是不會報錯的:

image-20241117164944148

但是呼叫它們其中一個的時候,編譯會報錯:

image-20241117165110394

17.庫合約

同其他語言裡面的庫函式,在Solidity中還有個重要作用就是能夠減少gas

和普通合約的區別:

  1. 不能有狀態變數
  2. 不能夠繼承或被繼承
  3. 不能接收以太幣
  4. 不可以被銷燬

庫合約中的函式若被設定為publicexternal,則在呼叫函式時會觸發一次delegatecall

若被設定為internal,則不會觸發;

若被設定為private,由於是私人的,只能庫合約內部自己訪問;

常用的一些庫合約:

  1. Strings:將uint256轉為string
  2. Address:判斷某個地址是否為合約地址;
  3. Create2:更安全的使用Create2 EVM opcode
  4. Arrays:跟陣列相關的庫合約;

17_01.Strings庫合約

此庫合約是將uint256型別轉換為相應的string型別的程式碼庫,樣例程式碼:

library Strings {
    bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";

    /**
     * @dev Converts a `uint256` to its ASCII `string` decimal representation.
     */
    function toString(uint256 value) public pure returns (string memory) {
        // Inspired by OraclizeAPI's implementation - MIT licence
        // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
     */
    function toHexString(uint256 value) public pure returns (string memory) {
        if (value == 0) {
            return "0x00";
        }
        uint256 temp = value;
        uint256 length = 0;
        while (temp != 0) {
            length++;
            temp >>= 8;
        }
        return toHexString(value, length);
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
     */
    function toHexString(uint256 value, uint256 length) public pure returns (string memory) {
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = "0";
        buffer[1] = "x";
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = _HEX_SYMBOLS[value & 0xf];
            value >>= 4;
        }
        require(value == 0, "Strings: hex length insufficient");
        return string(buffer);
    }
}

主要包含兩個函式:

  1. toString():將uint256轉換為string
  2. toHexString():將uint256轉換為hex,再轉換為string

17_02.使用庫合約

有兩種使用的方式;

  1. 使用using A for B

為型別B新增庫合約A;新增完後,B型別變數的成員便自動新增了庫A中的函式,可以直接呼叫;

呼叫時,這個變數會被當作第一個引數傳遞給函式;

  1. 透過庫合約名稱來直接呼叫函式;

比如:Strings.toString(xxx);

示例:

contract A{
    // 使用using A for B
    using Strings for uint256;
    function getString_1(uint256 num) public pure returns (string memory){
        return num.toString();
    }

    // 透過庫合約名來呼叫
    function getString_2(uint256 num) public pure returns (string memory){
        return Strings.toHexString(num);
    }
}
image-20241117173523142

18.Import

import可以在一個檔案中引用另一個檔案的內容,提高程式碼的可重用性和組織性;

  1. 透過檔案的相對位置可以引用:import './xxx.sol';
  2. 透過原始檔網址匯入網上的合約全域性符號;import 'https://xxxxx/xxx.sol';
  3. 透過npm的目錄匯入:import '@openzeppelin/contracts/access/Ownable.sol';
  4. 透過指定全域性符號匯入合約特定的全域性符號:import {XXX} from './xxx.sol';
// ---------------------------Demo.sol----------------------------------------
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract B{
    function sayHello() public pure returns (string memory){
        return "Hello!";
    }
}

// ---------------------------test.sol----------------------------------------
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// 透過檔案相對位置import
import './Demo.sol';
// 透過`全域性符號`匯入特定的合約
// 'B'是Demo.sol中合約的名稱
import {B} from './Demo.sol';
// 透過網址引用
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
// 引用OpenZeppelin合約
import '@openzeppelin/contracts/access/Ownable.sol';

contract A {
    // 成功匯入Address庫
    using Address for address;

    // 宣告Demo.sol中的合約變數
    // 要使用合約名稱
    B b = new B();

    // 呼叫引入的Demo.sol中合約B的函式
    function callImport() public view {
        b.sayHello();
    }
}
image-20241118212707032

19.接收ETH

Solidity支援兩種特殊的回撥函式:receive()fallback()

主要在兩種情況使用:

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

在0.6.x版本之前,語法上只有fallback()函式,用來接收使用者傳送的ETH以及在被呼叫函式簽名沒有匹配到時呼叫;

0.6版本之後,Solidity才將其拆分為receive()fallback()

19_01.接收ETH函式-receive

receive函式是在合約收到ETH轉賬時會被呼叫的函式,一個合約最多隻能有一個;

宣告的方式和一般函式不一樣,不需要function關鍵字,且不能有任何引數不能返回任何值,必須包含externalpayable

receive函式最好不要執行太多邏輯,因為對方呼叫sendtransfer方法傳送ETH的話,gas會被限制在2300,receive太複雜可能會觸發Out of gas報錯;

call就可以自定義gas執行更復雜的邏輯。

示例(在receive中傳送一個事件):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract A {
    event Received(address Sender, uint Value);
    receive() external payable { 
        emit Received(msg.sender, msg.value);
    }
}

在老版本中,有些惡意合約,會在receive函式中嵌入惡意消耗gas的內容或者使得執行故意失敗的程式碼,導致一些包含退款和轉賬邏輯的合約不能正常工作;

19_02.回退函式-fallback

fallback函式會在呼叫合約中不存在的函式時被觸發;

可用於接收ETH,也可用於代理合約(proxy contract);

receive函式一樣,不需要function關鍵字,但必須包含external,一般也會使用payable來修飾;

示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract A {
    event fallbackCalled(address Sender, uint Value, bytes Data);
    fallback() external payable { 
        emit fallbackCalled(msg.sender, msg.value, msg.data);
    }
}

19_03.兩者區別

首先,它們倆都能夠接收ETH;,它們觸發的規則如下:

​ / 是 --> receive()

​ / 是 --> receive()是否存在

接收ETH --> msg.data是否為空 \ 否 --> fallback()

​ \ 否 --> fallback()

只有msg.data為空且receive()存在時,才會使用receive()

兩者都不存在時,向合約傳送ETH會報錯;(但仍然可以透過帶有payable的函式向合約傳送ETH)

receive函式時,轉賬時data為空:

image-20241118222729536

轉賬時data不為空:

image-20241118223045330

20.傳送ETH

Solidity有三種方式向其他合約傳送ETH:transfer()send()call(),其中call推薦使用;

首先先部署一個接收ETH的合約:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract ReceiveETH {
    // 收到ETH的事件
    // 記錄amount和gas
    event Log(uint amount, uint gas);

    // 接收ETH時觸發的方法
    receive() external payable { 
        emit Log(msg.value, gasleft());
    }

    // 返回ETH餘額
    function getBalance() public view returns (uint){
        return address(this).balance;
    }
}

部署後執行getBalance(),發現此時的餘額為0:

image-20241119225817417

20_01.transfer

用法:接收方地址.transfer(傳送的ETH數額)

  • transfer的gas限制是2300,足夠用於轉賬,前提是接收方的fallbackreceive不能太複雜;
  • transfer如果轉賬失敗,會自動revert交易(回滾交易);

示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Transfer_test{
    function transferETH(address payable to, uint256 amount) external payable {
        // to是接收方
        to.transfer(amount);
    }
}

轉賬失敗時:

image-20241119231200736

轉賬成功時(多餘的轉賬會被返回到傳送方合約,並非附帶ETH的錢包):

image-20241119232141984

20_02.send

用法:接收方地址.send(傳送的ETH數額)

  • send的gas限制同樣是2300;
  • send如果轉賬失敗,不會revert
  • send的返回值是bool,代表的是轉賬成功或者失敗,需要額外的程式碼來處理;

示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Send_test{
    // 傳送ETH失敗的錯誤
    error SendFailed();
    
    // 傳送ETH
    function sendETH(address payable to, uint256 amount) external payable {
        bool success = to.send(amount);
        if (!success){
            // 失敗就revert錯誤
            revert SendFailed();
        }
    }
}

轉賬失敗:

image-20241119233240303

轉賬成功(同樣多餘的ETH退回到傳送方合約):

image-20241119233450913

20_03.call

用法:接收方地址.call{value:傳送到ETH數額}("")

  • call沒有gas限制,可以支援對方合約fallbackreceive實現複雜邏輯;
  • call如果轉賬失敗,不會revert
  • call對返回值是bool,和send一樣需要額外程式碼處理;

示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Call_test{
    // 傳送ETH失敗的錯誤
    error CallFailed();
    
    // 傳送ETH
    function callETH(address payable to, uint256 amount) external payable {
        bool success = to.call{value:amount}("");
        if (!success){
            // 失敗就revert錯誤
            revert CallFailed();
        }
    }
}

轉賬失敗時:

image-20241119234226065

轉賬成功時(同樣多餘的ETH退回到傳送方合約):

image-20241119234359832

21.呼叫其他合約

TestContract合約,目的是被其他合約所呼叫:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract TestContract {
    // 設定私有變數
    uint256 private x = 0;

    // 交易的事件,記錄amount和gas
    event Log(uint amount, uint gas);

    // 得到合約賬戶餘額
    function getBalance() public view returns (uint){
        return address(this).balance;
    }

    // 設定合約中私有變數值
    // 同時可以向其中轉賬
    function setX(uint256 num) external payable {
        x = num;
        if(msg.value > 0){
            emit Log(msg.value, gasleft());
        }
    }

    // 獲得私有變數的值
    function getX() external view returns (uint256){
        return x;
    }
}

部署,並得到合約地址:

image-20241120002354682

呼叫合約的程式碼:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// 由於在不同的檔案,所以先匯入
import './test.sol';

contract CallContract{
    // 方式一
    // 合約名(合約地址).func()
    function callSetX(address contract_address, uint256 x) external{
        TestContract(contract_address).setX(x);
    }

    // 方式二
    // 合約地址.func()
    function callGetX(TestContract contract_address) external view returns(uint x){
        x = contract_address.getX();
    }

    // 方式三
    // 建立合約物件的方式,然後呼叫
    function callGetX_2(address contract_address) external view returns(uint x){
        TestContract tc = TestContract(contract_address);
        x = tc.getX();
    }

    // 呼叫並轉賬
    function setXTransferETH(address contract_address, uint256 x) payable external{
        TestContract(contract_address).setX{value: msg.value}(x);
    }
}

21_01.呼叫方式一

可以在函式中傳入合約地址,生成目標合約的引用,然後再呼叫函式;

  • 用法:合約名(合約地址).func(引數)

  • 合約名和介面都必須保持一致(TestContractsetX());

// 方式一
// 合約名(合約地址).func()
function callSetX(address contract_address, uint256 x) external{
    TestContract(contract_address).setX(x);
}
image-20241120003213411

21_02.呼叫方式二

參考方式一中,將address型別換為目標合約名即可;

注意:TestContract contract_address的底層還是address型別,生成的ABI中,呼叫callGetX時傳入的引數都是address型別的;

  • 用法:
    • 引數->合約名 合約地址
    • 函式內部->合約地址.func(引數)
// 方式二
// 合約地址.func()
function callGetX(TestContract contract_address) external view returns(uint x){
    x = contract_address.getX();
}
image-20241120003946647

21_03.呼叫方式三

透過建立合約(物件)的方式;

用法:合約名 變數名 = 合約名(地址);

// 方式三
// 建立合約物件的方式,然後呼叫
function callGetX_2(address contract_address) external view returns(uint x){
    TestContract tc = TestContract(contract_address);
    x = tc.getX();
}
image-20241120004228115

21_04.呼叫並轉賬

如果目標函式是payable的,那麼便可以向其轉賬;

用法:合約名(合約地址).func{value:xxx}(引數);

// 呼叫並轉賬
function setXTransferETH(address contract_address, uint256 x) payable external{
    TestContract(contract_address).setX{value: msg.value}(x);
}
image-20241120004826076

22.Call

20_03call可以用來傳送ETH,同時它還可以呼叫合約;

calladdress型別的低階成員函式,它用來與其他合約互動;

  • 返回值:(bool, bytes memory),分別對應call是否成功以及目標函式的返回值;
  • call是官方推薦的透過觸發fallbackreceive函式傳送ETH的方法;
  • 不推薦用call來呼叫另一個合約(因為當你呼叫一個不安全的合約時,主動權便不在你的手上;推薦宣告合約變數後呼叫函式21_03);
  • 當我們不知道對方合約的原始碼或者ABI,就沒法生成合約變數;此時,仍然可以透過call呼叫對方合約的函式;

22_01.使用規則

用法:目標合約地址.call(位元組碼),可以在不知道原始碼或ABI的情況下呼叫;

  • 位元組碼:利用結構化編碼函式來獲得 --> abi.encodeWithSignature("函式簽名", 具體引數, 具體引數, ...)
    • 函式簽名:是函式名(引數型別,引數型別,...)

示例:abi.encodeWithSignature("f(uint256,address)",x,addr)

在呼叫合約的同時,call還能知道交易傳送的ETH和gas:

使用方法:目標合約地址.call{value:ETH數額, gas:gas數額}(位元組碼),就是在引數前加了大括號,裡面填上傳送的數額;

22_02.透過call呼叫目標合約

目標合約(還是和之前一樣):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract TestContract {
    // 設定私有變數
    uint256 private x = 0;

    // 交易的事件,記錄amount和gas
    event Log(uint amount, uint gas);

    // 得到合約賬戶餘額
    function getBalance() public view returns (uint){
        return address(this).balance;
    }

    // 設定合約中私有變數值
    // 同時可以向其中轉賬
    function setX(uint256 num) external payable {
        x = num;
        if(msg.value > 0){
            emit Log(msg.value, gasleft());
        }
    }

    // 獲得私有變數的值
    function getX() external view returns (uint256){
        return x;
    }
}

呼叫setX(uint256 num)函式,有引數,但無返回值(data無內容),附帶ETH傳送過去:

image-20241121220329742

呼叫getX()函式,無引數,但有返回值(data有內容),不帶ETH:

image-20241121221156903

呼叫一個不存在的函式:

  • 當沒有fallback函式的情況下(會返回false):
image-20241121222026282
  • 當給目標合約新增一個fallback函式時,再呼叫它(會返回true):
fallback() external payable { }
image-20241121222709235

完整示例程式碼:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// 沒有匯入test.sol
contract CallContract{
    // 定義的Response事件
    // 輸出call返回的結果和data
    event Response(bool success, bytes data);

    function callSetX(address payable addr, uint256 x) public payable {
        // 呼叫setX函式
        // 同時可以傳送ETH
        // {}中是傳送的ETH數額
        // ()中是利用結構化編碼函式獲得的位元組碼
        (bool success, bytes memory data) = addr.call{value:msg.value}(abi.encodeWithSignature("setX(uint256)", x));

        emit Response(success, data);
    }

    function callGetX(address addr) external returns (uint256){
        // 呼叫getX函式
        // ()中是利用結構化編碼函式獲得的位元組碼
        (bool success, bytes memory data) = addr.call(abi.encodeWithSignature("getX()"));

        emit Response(success, data);

        // 返回data中的值(轉為uint)
        return abi.decode(data, (uint256));
    }

    function callNonExist(address addr) external {
        // 呼叫一個不存在的函式
        (bool success, bytes memory data) = addr.call(abi.encodeWithSignature("xxx(address)"));

        emit Response(success, data);
    }
}

23.DelegateCall

delegatecall委託,和call差不多,同樣是地址型別的低階成員函式;

23_01.什麼是委託

當使用者A透過合約Bcall合約C時:

  • 此時執行的是合約C上的函式;

  • 上下文(Context,可以理解為包含變數和狀態的環境)也是合約C的:

    • msg.sender是合約B的地址

    • 若函式改變了一些狀態變數,產生的效果會用在合約C的變數上;

image-20241121223728699

而當使用者A透過合約Bdelegatecall合約C時:

  • 執行的是合約C上的函式;
  • 上下文仍然是合約B的:
    • msg.sender是合約A的地址;
    • 若函式改變了一些狀態變數,產生的效果會用在合約B的變數上;
image-20241121223807749

也可以這麼理解:

  1. 合約B的視角

我合約B"借用"了合約C的某一個函式的功能,來改變我自己這邊的一些狀態;

  1. 現實世界

使用者A:投資者

合約B中的狀態變數:資產

合約C中執行的函式:風險投資機構

投資者將他的資產交給一個風險投資機構來打理,此時執行的是風險投資機構,但改變的是投資者資產

23_02.使用規則

call類似:目標合約地址.delegatecall(位元組碼)

其中位元組碼仍是透過abi.encodeWithSignature()來獲得的;

call不一樣的是:delegatecall()在呼叫時,不能指定傳送的ETH數額,但能指定gas數額;

注意:delegatecall()有安全隱患,使用時要保證當前合約和目標合約的狀態變數儲存結構相同,並且目標合約安全,不然會造成財產損失。

23_03.什麼情況下用到委託

主要有兩個應用場景:

  1. 代理合約(Proxy Contract

將智慧合約的儲存合約邏輯合約分開;

儲存合約(代理合約(Proxy Contract))儲存所有相關的變數,並且儲存邏輯合約的地址;

邏輯合約(Logic Contract)中儲存所有的函式,透過delegatecall執行;

當升級的時候,只需要將代理合約指向新的邏輯合約即可(以太坊官方開發文件中有提到)。

  1. EIP-2535 Diamonds(鑽石)

鑽石是一個支援構建可在生產中擴充套件的模組化智慧合約系統的標準。鑽石具有多個實施合約的代理合約。詳細資訊:鑽石標準簡介

23_04.示例

使用者A透過合約B委託呼叫合約C

被呼叫的合約C

兩個狀態變數和一個可以修改狀態變數的函式:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// 被呼叫的合約C
contract C {
    // 狀態變數num
    uint public num;
    // 狀態變數sender
    address public sender;

    // 設定狀態變數num和sender的值
    function setVars(uint x) public payable {
        num = x;
        sender = msg.sender;
    }
}

發起呼叫的合約B

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// 沒有匯入test.sol
contract B{
    // 必須與合約C的變數儲存佈局相同
    // 兩個變數,順序也必須一致
    uint public num;
    address public sender;

    // 透過call來呼叫SetVars函式
    // 預計只會改變合約C的變數值
    function callSetVars(address addr, uint x) external payable {
        (bool success, bytes memory data) = addr.call(abi.encodeWithSignature("setVars(uint256)", x));
    }

    // 透過delegatecall來呼叫SetVars函式
    // 預計只改變本合約(合約B)的變數值
    function delegatecallSetVars(address addr, uint x) external payable {
        (bool success, bytes memory data) = addr.delegatecall(abi.encodeWithSignature("setVars(uint256)", x));
    }
}

驗證

狀態變數的初始值:

image-20241121235657671

合約B中呼叫callSetVars函式,預計只會改變合約C中的變數值(num為更改後的值,sender為合約B的地址):

image-20241122000241525

在合約B中呼叫delegatecallSetVars函式,預計會改變合約B中的變數(num變為更改後的值,sender為錢包地址),合約C中的不變:

image-20241122000953665

24.在合約中建立新合約

以太坊上,外部賬戶EOA(錢包)可以建立智慧合約;此外,智慧合約也可以建立新的智慧合約。

去中心化交易所Uniswap就是利用工廠合約(PairFactory)建立了無數個幣對合約(Pair)

Uniswap V2核心合約中包含兩個合約:

  1. UniswapV2Pair:幣對合約,用於管理幣對地址,流動性,買賣;
  2. UniswapV2Factory:工廠合約,用於建立新的幣對合約,並管理幣對地址;

24_01.Create

Create用法:ContractXXX xxx = new ContarctXXX{value:_value}(建構函式引數)

就和new物件一樣,新new一個合約,並傳入新合約建構函式所需要的引數,並且可以附帶ETH(前提建構函式得是payable的);

極簡Uniswap

Create來實現一個極簡版的Uniswap(真正的Uniswap不是用這種方式實現的,是24_02中的方法):

幣對合約(Pair)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// 幣對合約
// 包含3個狀態變數
// 部署時將factory賦值
// 呼叫initToken時更新幣對中兩個代幣的地址
contract Pair{
    // 工廠地址
    address public factory;
    // 代幣0
    address public token0;
    // 代幣1
    address public token1;

    // 建構函式,帶有payable
    // 將訊息的傳送者賦值為factory
    constructor() payable {
        factory = msg.sender;
    }

    // 初始化代幣0和代幣1的地址
    function initToken(address _token0, address _token1) external {
        // 檢測是否是factory呼叫的
        require(factory == msg.sender, "Not real factory use function");
        // 代幣地址賦值
        token0 = _token0;
        token1 = _token1;
    }
}

工廠合約(PairFactory)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import './Pair.sol';

// 工廠合約
// 一個對映將 代幣 和 幣對合約地址 建立聯絡
// 一個陣列 儲存 幣對合約地址
// 利用Create方法建立新的合約
contract PairFactory {
    // 對映,address -> address -> address
    mapping (address => mapping ( address => address )) public getPair;
    // 儲存所有的Pair地址(幣對合約地址)
    address[] public allPairs;

    // 建立新的幣對合約地址
    function createPair(address token0, address token1) external returns (address pairAddr){
        // 利用Create方法建立新合約
        Pair pair = new Pair();
        // 呼叫新合約的initToken方法,並初始化裡面的token0,token1
        pair.initToken(token0, token1);
        // 獲得當前幣對合約的地址
        pairAddr = address(pair);
        // 儲存在陣列中
        allPairs.push(pairAddr);
        // 建立對映
        // token0 -> token1 -> 幣對合約地址
        getPair[token0][token1] = pairAddr;
        // token1 -> token0 -> 幣對合約地址
        getPair[token1][token0] = pairAddr;
    }
}

利用下面兩個地址作為引數呼叫createPair函式:

WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC鏈上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
image-20241126002026701 image-20241126002224011

24_02.Create2

上面可以看到Create方法建立的合約地址是完全不可預測的;

Create2方法使我們在部署智慧合約之前就能預測合約的地址(Uniswap建立 Pair合約(幣對合約)的方法就是這個)。

Create2方法的目的是為了讓合約地址獨立於未來事件,不管未來區塊鏈上發生什麼,都可以將合約部署在事先計算好的地址上。

Create原理

新地址 = hash(建立者地址, nonce)

無論是EOA建立還是智慧合約建立,都是這個方法;

建立者地址部署的錢包地址或者合約地址

nonce,對於EOA是該地址傳送的交易總數,對於合約賬戶是建立的合約總數,建立時的noncenonce+1

建立者的地址不會變,但是nonce會隨著時間而改變,所以不好預測;

Create2原理

新地址 = hash("0xFF", 建立者地址, salt, initcode)

0xFF:一個常數,避免和Create衝突;

建立者地址:呼叫Create2的當前合約地址;

salt:一個由建立者指定的bytes32型別的值,主要目的是用來影響新建立的合約地址;

initcode:新合約的初始位元組碼(合約的Creation Code和建構函式引數);

Create2用法

ContractXXX xxx = new COntractXXX{salt:_salt, value:_value}(建構函式引數)

同樣也是new,只不過多加入了個salt

極簡Uniswap2

使用Create2來實現一個極簡的Uniswap

幣對合約(Pair)(和之前一樣):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// 幣對合約
// 包含3個狀態變數
// 部署時將factory賦值
// 呼叫initToken時更新幣對中兩個代幣的地址
contract Pair{
    // 工廠地址
    address public factory;
    // 代幣0
    address public token0;
    // 代幣1
    address public token1;

    // 建構函式,帶有payable
    // 將訊息的傳送者賦值為factory
    constructor() payable {
        factory = msg.sender;
    }

    // 初始化代幣0和代幣1的地址
    function initToken(address _token0, address _token1) external {
        // 檢測是否是factory呼叫的
        require(factory == msg.sender, "Not real factory use function");
        // 代幣地址賦值
        token0 = _token0;
        token1 = _token1;
    }
}

工廠合約(PairFactory)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import './Pair.sol';

// 工廠合約
// 一個對映將 代幣 和 幣對合約地址 建立聯絡
// 一個陣列 儲存 幣對合約地址
// 利用Create方法建立新的合約
contract PairFactoryV2 {
    // 對映,address -> address -> address
    mapping (address => mapping ( address => address )) public getPair;
    // 儲存所有的Pair地址(幣對合約地址)
    address[] public allPairs;

    // 建立新的幣對合約地址
    function createPairV2(address token0, address token1) external returns (address pairAddr){
        // 檢測兩個地址不同
        require(token0 != token1, "Identcial Address");
        // 將地址按照從小到大排序
        (address token_0, address token_1) = token0 < token1 ? (token0, token1) : (token1, token0);
        // 計算一個salt
        bytes32 salt = keccak256(abi.encodePacked(token_0, token_1));
        // 利用Create2方法建立新合約
        Pair pair = new Pair{salt: salt}();
        // 呼叫新合約的initToken方法,並初始化裡面的token0,token1
        pair.initToken(token_0, token_1);
        // 獲得當前幣對合約的地址
        pairAddr = address(pair);
        // 儲存在陣列中
        allPairs.push(pairAddr);
        // 建立對映
        // token0 -> token1 -> 幣對合約地址
        getPair[token0][token1] = pairAddr;
        // token1 -> token0 -> 幣對合約地址
        getPair[token1][token0] = pairAddr;
    }

    // 預測地址
    function calcAddr(address token0, address token1) public view returns (address predictedAddr){
        // 檢測兩個地址不同
        require(token0 != token1, "Identcial Address");
        // 將地址按照從小到大排序
        (address token_0, address token_1) = token0 < token1 ? (token0, token1) : (token1, token0);
        // 計算一個salt
        bytes32 salt = keccak256(abi.encodePacked(token_0, token_1));
        // 計算地址
        predictedAddr = address(uint160(uint(
            // hash
            keccak256(abi.encodePacked(
                // 四個引數
                bytes1(0xff),
                address(this),
                salt,
                keccak256(type(Pair).creationCode)
            )))
        ));
    }
}

若部署的合約的建構函式中需要有引數:

比如Pair pair new Pair{salt:salt}(address(this));

predictedAddr = address(uint160(uint(
    // hash
    keccak256(abi.encodePacked(
        // 四個引數
        bytes1(0xff),
        address(this),
        salt,
        // 一起打包,並計算雜湊
        keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this))))
    )))
));

還是利用這兩個地址:

WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC鏈上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c

事先計算:

image-20241126012744690

驗證:

image-20241126012839407 image-20241126013109845

24_03.應用場景

  1. 交易所為新使用者預留建立錢包合約的地址;
  2. 減少不必要的呼叫(知道新合約的地址後,無需再執行getPair的跨合約呼叫);

25.刪除合約

25_01.selfdestruct

selfdestruct命令可被用來刪除合約,並將該合約剩餘的ETH轉到指定地址;

它為了應對合約出錯的極端情況而設計的,最早被命名為suicide,後面改為selfdestruct

v0.8.18版本中,它被標記為"不再建議使用",因為在一些情況下它會導致預期之外的合約語意,但由於目前還沒有替代方案,只對開發者做了編譯階段的警告,相關內容:EIP-6049

然而,在以太坊坎昆(Cancun)升級中,EIP-6780被納入升級以實現對Verkle Tree更好的支援。該更新減少了SELFDESTRUCT操作碼的功能。

根據提案描述,當前SELFDESTURCT僅會被用來將合約中的ETH轉移到指定地址,而原先的刪除功能只有在合約建立-自毀這兩個操作處在同一筆交易時才能生效。

所以,目前來說:

  1. 現在的seldestrict僅會被用來將合約中的ETH轉移到指定地址;
  2. 已經部署的合約無法被SELFDESTRUCT
  3. 如果要使用原先的SELFDESTRUCT功能,必須在同一筆交易中建立並自毀;

25_02.如何使用selfdeftruct

用法:selfdestruct(addr)

其中,addr是接收合約中剩餘ETH的地址,並且addr地址不需要有receive()fallback()也能接收ETH。

25_03.升級前後功能對比

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract SelfDestructDemo{
    uint public value = 10;
    constructor() payable {}
    receive() external payable { }  

    // 升級之前應該能自毀
    // 升級之後只能轉移ETH
    function SelfDestruct() external {
        selfdestruct(payable(msg.sender));
    }

    // 獲取餘額
    function getBalance() external view returns (uint balance){
        balance = address(this).balance;
    }
}

升級前:

合約中函式報錯,並且合約中的ETH被轉入指定地址;

image-20241127002142455 image-20241127002254054

升級後:

合約中的ETH被轉入指定地址,但合約中的函式仍能使用;

image-20241127002604531

25_04.同筆交易實現建立-自毀

// SPDX-License-Identifier: MIT
// pragma solidity ^0.8.26;
pragma solidity ^0.8.4;

// DeployDestructDemo合約(還是上一個)
import './Factory.sol';

contract DeployDestructDemo{

    struct DemoResult{
        address addr;
        uint balance;
        uint value;
    }

    constructor() payable {}

    function getBalance() external view returns (uint balance){
        balance = address(this).balance;
    }

    // 演示建立-自毀
    function demo() public payable returns (DemoResult memory){
        // 建立一個新合約
        SelfDestructDemo sd = new SelfDestructDemo{value:msg.value}();
        // 給返回值賦值
        DemoResult memory res = DemoResult({
            addr:address(sd),
            balance:sd.getBalance(),
            value:sd.value()
        });
        // 新合約呼叫自銷燬
        sd.SelfDestruct();
        return res;
    }
}
image-20241127004222773 image-20241127004548737 image-20241127004836011

26.ABI編碼解碼

ABI-(Application Binary Interface,應用二進位制介面),是與以太坊智慧合約互動的標準。

資料基於他們的型別編碼,並且由於編碼後不包含型別資訊,解碼時需要註明它們的型別;

編碼abi.encodeabi.encodePackedabi.encodeWithSignatureabi.encodeWithSelector

解碼abi.decode

26_01.abi編碼

下面將這4個變數一起打包編碼:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract ABIEncode{
    uint256 x = 10;
    address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    string name = "0xAA";
    uint[2] array = [3, 4];

    function encode() public view returns (bytes memory res){
        res = abi.encodeXXX(x, addr, name, array);
    }
}

abi.encode(能和合約互動)

將給定引數利用ABI規則編碼;

將每個引數填充為32位元組的倍數的資料,並拼接在一起;

如果要和智慧合約互動,需要使用它;

uint256 x = 10;
address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
string name = "0xAA";
uint[2] array = [3, 4];
// 結果
//0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
/*
0x
000000000000000000000000000000000000000000000000000000000000000a(x)
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
00000000000000000000000000000000000000000000000000000000000000a0(array)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000004(string)
3078414100000000000000000000000000000000000000000000000000000000
*/

若將string變成很長:

uint256 x = 10;
address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
string name = "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
uint[2] array = [3, 4];
// 結果
//0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000047307841414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414100000000000000000000000000000000000000000000000000
/*
0x
000000000000000000000000000000000000000000000000000000000000000a(x)
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
00000000000000000000000000000000000000000000000000000000000000a0(array)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000047(string)
3078414141414141414141414141414141414141414141414141414141414141
4141414141414141414141414141414141414141414141414141414141414141
4141414141414100000000000000000000000000000000000000000000000000
*/

abi.encodePacked(不能和合約互動)

將給定引數根據其所需要的最低空間編碼,與abi.encode類似,但會省略很多0;

但不能與合約互動;

uint256 x = 10;
address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
string name = "0xAA";
uint[2] array = [3, 4];
// 結果
//0x000000000000000000000000000000000000000000000000000000000000000a5b38da6a701c568545dcfcb03fcb875f56beddc43078414100000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000004
/*
0x
000000000000000000000000000000000000000000000000000000000000000a(x,因為是uint256)
5b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
30784141(string)
00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000004(array)
*/

abi.encodeWithSignature(呼叫其他合約時使用)

abi.encode類似,但是第一個引數是函式簽名keccak雜湊,編碼時為4位元組,等同於在前面加了個函式選擇器

當呼叫其他函式的時候可以使用;

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract ABIEncode{
    uint256 x = 10;
    address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    string name = "0xAA";
    uint[2] array = [3, 4];

    function encode() public view returns (bytes memory res){
        res = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
    }
}

// 結果
//0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
/*
0x
e87082f1(函式簽名)
000000000000000000000000000000000000000000000000000000000000000a(x)
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
00000000000000000000000000000000000000000000000000000000000000a0(array)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000004(string)
3078414100000000000000000000000000000000000000000000000000000000
*/

abi.encodeWithSelector

abi.encodeWithSignature類似,只不過第一個引數時函式選擇器,為函式簽名Keccak雜湊的前4個位元組;

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract ABIEncode{
    uint256 x = 10;
    address addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    string name = "0xAA";
    uint[2] array = [3, 4];

    function encode() public view returns (bytes memory res){
        res = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
    }
}

// 結果
//0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
/*
0x
e87082f1(函式簽名)
000000000000000000000000000000000000000000000000000000000000000a(x)
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4(addr)
00000000000000000000000000000000000000000000000000000000000000a0(array)
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000004(string)
3078414100000000000000000000000000000000000000000000000000000000
*/

26_02.abi解碼

abi.decode

用於解碼abi.encode生成的二進位制編碼,將它還原成原本的引數;

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract ABIEncode{
    function dncode(bytes memory data) public pure returns (uint x, address addr, string memory name, uint[2] memory array){
        (x, addr, name, array) = abi.decode(data, (uint, address, string, uint[2]));
    }
}
// 輸入
// 0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
image-20241129224205133

27.選擇器

27_01.calldata

當我們呼叫智慧合約時,本質上是向目標合約傳送了一段calldata,傳送交易後,可以在詳細資訊的input中看到此次交易的calldata

image-20241129224706383

傳送的calldata中前4個位元組是函式選擇器(selector)

// 上圖中的calldata
// 0x012b48bf000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
/*
0x
012b48bf(函式選擇器)
(因為bytes是動態的,所以會有下面這倆,靜態的不會有,比如address,uint)
0000000000000000000000000000000000000000000000000000000000000020(偏移量,0x20 = 32,從這開始偏移32個位元組)
00000000000000000000000000000000000000000000000000000000000000e0(引數長度,0xe0 = 7 * 32,正好對應上面)

輸入的引數
000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
*/

其實,calldata就是告訴智慧合約,為要呼叫哪個函式,引數都是什麼;

27_02.selector的生成

基礎型別引數

基礎型別的引數有:uint(uint8, ..., uint256)booladdress等;

bytes(keccak256("func_name(uint256,bool,...)"));

固定長度型別引數

固定長度型別的引數,比如:uint256[3]

bytes(keccak256("func_name(uint256[3])"));

可變長度型別引數

可變長度型別的引數,比如:address[]uint[]stringbytes等;

bytes(keccak256("func_name(bytes,string)"));

對映型別引數

對映型別的引數有:contractenumstruct等;

contract Demo{}	// 需要轉化為address
struct User{	// 需要轉化為tuple型別:(uint256,bytes)
    uint256 uid;
    bytes name;
}
enum School {SCHOOL1, SCHOOL2}	// 需要轉化為uint8
mapping(address => uint) public balance;	// 直接轉化為address(第一個型別),因為mapping型別不能直接作為引數
bytes(keccak256("func_name(address,(uint256,bytes),uint256[],uint8),address"))

27_03.使用selector

address(this).call(abi.encodeWithSelector(0x12345678函式簽名, 引數, 引數, ...));

28.Try Catch

28_01.用法

基礎用法

try func_name(){
    // call成功的情況下
} catch{
    // call失敗的情況下
}

呼叫的函式有返回值

必須這麼使用(需要加上returns),同時可以使用返回的變數:

try func_name() returns (address addr, uint x){
    // call成功的情況下
    // 可以使用返回的變數
} catch{
    // call失敗的情況下
}

捕捉特殊的異常原因

try func_name() returns (address addr, uint x){
    // call成功的情況下
    // 可以使用返回的變數
} catch Error(string memory reason){
    // 捕捉revert("xxxx")
    // 捕捉require(false, "xxxx")
} catch Panic(uint errorCode){
    // 捕捉Panic導致的錯誤
    // 例如assert失敗、溢位、除零、陣列訪問越界等
} catch (bytes memory lowLevelData){
    // 如果發生了revert且上面2個異常匹配失敗,會進入這個分支
    // 例如revert()、require(false)、revert(自定義的error)
}

28_02.示例

呼叫合約(合約建立成功,但函式呼叫錯誤)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract OnlyEven{
    constructor(uint a){
        // 當a = 0時,require會丟擲異常
        require(a != 0, "invalid number");
        // 當a = 1時,assert會丟擲異常
        assert(a != 1);
    }

    function onlyEven(uint b) external pure returns(bool success){
        // 當b為奇數時,require丟擲異常
        require(b % 2 == 0, "Odd number");
        success = true;
    }
}

contract TryCatch{
    // 成功事件
    event SuccessEvent();
    // 丟擲異常時的兩個事件
    // 對應require和revert
    event CatchEvent(string message);
    // 對應assert
    event CatchByte(bytes data);
    // 合約狀態變數
    OnlyEven oe;
    // 建構函式
    constructor(){
        // 賦值為2,應該不會丟擲異常
        oe = new OnlyEven(2);
    }

    function exec(uint amount) external returns (bool success){
        try oe.onlyEven(amount) returns (bool _success){
            // 成功,返回True
            emit SuccessEvent();
            return _success;
        } catch Error(string memory reason){
            // 失敗,捕捉require(false, error_string)
            // 比如此處輸入的是奇數,應該返回"Odd number"
            emit CatchEvent(reason);
        }
    }
}

成功:

image-20241130003918089

失敗:

image-20241130003955781

呼叫合約(合約建立失敗)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract OnlyEven{
    constructor(uint a){
        // 當a = 0時,require會丟擲異常
        require(a != 0, "invalid number");
        // 當a = 1時,assert會丟擲異常
        assert(a != 1);
    }

    function onlyEven(uint b) external pure returns(bool success){
        // 當b為奇數時,require丟擲異常
        require(b % 2 == 0, "Odd number");
        success = true;
    }
}

contract TryCatch{
    // 成功事件
    event SuccessEvent();
    // 丟擲異常時的兩個事件
    // 對應require和revert
    event CatchEvent(string message);
    // 對應assert
    event CatchByte(bytes data);
		// exec(0) --> 失敗,釋放CatchEvent
		// exec(1) --> 失敗,釋放CatchByte
		// exec(2) --> 成功,釋放SuccessEvent
    function exec(uint num) external returns (bool success){
        try new OnlyEven(num) returns (OnlyEven oe){
            emit SuccessEvent();
            success = oe.onlyEven(num);
        } catch Error(string memory reason){
            // 捕捉失敗的revert()和require()
            emit CatchEvent(reason);
        } catch (bytes memory reason){
            // 捕捉失敗的assert()
            emit CatchByte(reason);
        }
    }
}

exec(0) --> 失敗,釋放CatchEvent:

image-20241130004935488

exec(1) --> 失敗,釋放CatchByte:

image-20241130005020918

exec(2) --> 成功,釋放SuccessEvent:

image-20241130005053655

參考:https://github.com/AmazingAng/WTF-Solidity

相關文章