Skip to content

Latest commit

 

History

History
1601 lines (1389 loc) · 65.9 KB

Yuchen.md

File metadata and controls

1601 lines (1389 loc) · 65.9 KB
timezone
Asia/Shanghai

Yuchen

  1. 自我介绍
    大家好,我是Yuchen,目前就讀於資工系3年級,但在此前完全沒有學習過solidity,但一直想學習撰寫智慧合約並學習與區塊鏈相關的知識,希望透過此次機會與大家一起有規劃的學習:)。

  2. 你认为你会完成本次残酷学习吗?
    會,我會盡力在課業之外規劃時間進行學習,相信活動中設計的壓力也可以推動著我努力跟上大家的學習進度。

Notes

2024.09.23

什麼是智能合約?

智能合約(Smart Contracts)是一種自動執行的協議,將雙方的協議條款,用代碼形式在區塊鏈上運行,當條件滿足時可以自動執行之前定義的操作,ex.轉帳、處理合約...
智能合約儲存在一個公共資料庫中,且不能被更改。

智能合約3要素:

  1. 自治:合約一啟動就會自動運行,不需要任何人為的干預。
  2. 自足:智能合約可以自主控制其計算所涉及的資源,比如有權限調配合約雙方的資金和財產。
  3. 去中心化:通過分散式的節點來自動運行,而不用透過中心化的單個伺服器。

乙太坊(Ethereum)把基於智能合約的應用程式稱為去中心化應用程式(Decentralized App , Dapp)。
智能合約能用來串聯Dapp與區塊鏈,Dapp不同於傳統app,Dapp具備去中心化、數據不可竄改、公開透明的特性,因此會比使用傳統中心化的app更安全。

  • APP:前端介面加上一個中心化的伺服器。
  • Dapp: 前端介面加上去中心化的智能合約,因為放在區塊鏈上,不需要伺服器。

Solidity 簡介

Solidity 為用於編寫智能合約的高階語言,主要針對 Ethereum 區塊鏈平台開發設計。

開發工具:Remix
網址:https://remix.ethereum.org/

第一支程式:Hello Web3!

// SPDX-License-Identifier: MIT  
pragma solidity ^0.8.21;
contract HelloWeb3{
    string public _string = "Hello Web3!";
}
  1. 第 1 行是註釋,說明這段程式所使用的程式許可(MIT許可),若沒加入許可,編譯時會出現警告,但仍可運行。
  2. 第 2 行聲明使用的 Solidity 版本,因為不同版本的語法有差異。
    這段程式表示不允許使用小於 0.8.21 或大於等於 0.9.0 的編譯器編譯。
  3. 第 3 行創建合約,並聲明合約名為 HelloWeb3。 第 4 行是合約內容,宣告公開 string 變量 _string,賦值"Hello Web3!"。
    contract HelloWeb3{
    string public _string = "Hello Web3!";
    }

部署這個合約後,外部用戶可以調用 Solidity 自動生成的 function _string() public view returns (string) 來讀取 _string 的值,它會返回 "Hello Web3!"。這是最基本的合約示例,用來展示如何在區塊鏈上存儲並公開字符串數據。

Solidity 的變量類型

  1. 值類型(Value Type):這類變數賦值時直接傳遞數值。

    • 數值類型:

      • uint:無符號整數(不包括負數),$0到2^{256}-1$
      • int:有符號整數(包括負數),$-2^{255}到2^{255}-1$。
      • 比較運算符: <=<==!=>=>
      • 算術運算符:+-*/%(取餘),**(幂)
    • 布林類型:true、false。 運算符包含:

      符號 邏輯
      ! 邏輯非
      && 邏輯與
      || 邏輯或
      == 等於
      != 不等於

      此外&&||遵循短路規則。

    • 位元操作類型:

      • byte / bytes1bytes32,表字節從1到最多32bytes。
      • bytes:可變長度的字節數據,存儲任意長度的原始二進制數據。
      bytes32 public _byte32 = "MiniSolidity"; 
      bytes1 public _byte = _byte32[0];
    • 地址類型:

      • address:儲存以太坊地址。長度為 20 字節(以太坊地址的大小),通常用來標識合約或帳戶。
      • address payable:特殊的 address,比普通地址多了 transfersend 兩個方法,允許接收以太幣的轉賬。
      // 地址
      address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
      address payable public _address1 = payable(_address); // payable address,可以转账、查余额
      // 地址类型的成员
      uint256 public balance = _address1.balance; // balance of address
    • 枚舉 enum :Solidity 中用戶定義的數據類型,使用自訂名稱代替從 0 開始的 uint。

    // 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
    enum ActionSet { Buy, Hold, Sell }
    // 创建enum变量 action
    ActionSet action = ActionSet.Buy;

    enum 可以和 uint 互相轉換

    // 將枚舉轉換為 uint
    function enumToUint() external view returns(uint){
        return uint(action); // 將 action 的枚舉值強制轉換為對應的 uint 類型。
    }
    // 將 uint 轉換為 enum
    function uintToEnum(uint _action) external {
        require(_action <= uint(ActionSet.Sell), "Invalid enum value"); // 檢查 _action 是否在 Action 枚舉的範圍內。
        action = ActionSet(_action);
    }
  2. 引用類型(Reference Type):不直接存儲數值,而是存儲指向數據的引用,進行賦值或函數調用時,傳遞的是數據的引用。

    • 字符串類型:string
    • 數組類型(array):可在宣告時指定大小T[k],也可動態調整T[],其中的元素可以是任何類型。
      • 由 5 個 int 組成的動態 array 稱為 int[][5]
      • push() 追加0元素,並返回追加的值。
      • push(value)
      • x[start:end]
    • 結構體(struct):
      • 合約之外宣告,此 struct 可被多合約共享。
      • 在合約內部宣告,此 struct 只能被本合約與繼承合約共享。
  3. 映射類型(Mapping Type):只能存在 stroage 數據位置。

    • 映射類型使用語法:mapping(KeyType KeyName? => ValueType ValueName?)
    • 映射類型變量使用語法:mapping(KeyType KeyName? => ValueType ValueName?) VariableName
      KeyType 可為內置的值類型(ex.string, enum...),用戶定義、複雜的類型不可(ex.映射, struct, array...),ValueType 可為任何類型(ex.string, 映射, struct...)
      mapping 詳細介紹

2024.09.24

函數

Solidity 函數形式:

function <function name>(<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)]
  1. function:宣告 function 的固定用法。
  2. <function name>:函數名。
  3. (<parameter types>):寫入函數的參數,包含變量類型與名稱。
  4. {internal|external|public|private}:函數可見性說明符。
    函數需要明確定義可見性,沒有默認值。
    • public:內部與外部均可見。
    • private:只能從本合約內部訪問,繼承的合約不可使用。
    • external:只能從合約外部訪問(內部可通過 this.f() 調用)。
    • internal:只能從本合約內部訪問,繼承的合約可使用。

※在修飾變量時可使用 public|private|internal 默認為 internalpublic 時會自動生成同名的 getter 函數,以查詢數值。

  1. [pure|view|payable]:決定函數權限/功能的關鍵字。

    • payable:可支付的。
    • pure‵:不能讀或寫入鏈上的狀態變量。
    • view:能讀不能寫入鏈上的狀態變量。
  2. [returns ()]:函数返回的变量类型和名称。

PureView 是什麼?

在乙太坊上交易如果改變了鏈上狀態則需要支付氣費(gas free),但 gas free 很貴,因此創建了 pureview,包含這兩個關鍵字的函數不改寫鏈上狀態,因此用戶呼叫後不需要付 gas。
※非 pure/view 的函數呼叫 pure/view 時需要付gas。

以下行為視為修改鏈上狀態

  1. 寫入狀態變量
  2. 釋放事件
  3. 創建其他合約
  4. 使用 selfdestruct
  5. 通過調用發送乙太幣
  6. 呼叫任何未標記 pure/view 的函數
  7. 使用低級呼叫(low-level calls)
  8. 使用包含某些操作碼的內聯匯編

minusPayable() 間接呼叫 minus(),並返回 ETH 餘額,透過 this 關鍵字可以引用合約地址,在呼叫 minusPayable() 時往合約中轉入 12 個 ETH。

函數輸出

返回值:return/returns

  • returns:跟在函式名之後,聲明返回的變量類型與變量名。
  • return:在函式主體中,返回指定的變量。
// 返回多變量
function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
    return(1, true, [uint256(1), 2, 5])
}

uint256[3] 聲明長度 3 且類型為 uint256 的數組為返回值,但若直接寫[1, 2, 5]會默認為 uint8[3],因此第一個值須強制轉成 uint256,聲明該數組中的元素皆為 uint256

命令式返回
returns 中標明返回變量的名稱。Solidity 會初始化這些變量,並自動返回,無須使用 return

function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
    _number = 2;
    _bool = false;
    _array = [uint256(3),2,1];
}

解構式返回

  • 讀取所有返回值:聲明變量,後將要賦值的變量用 , 隔開,依序排列。
uint256 _number;
bool _bool;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();
  • 讀取部分返回值:聲明要讀取的返回值對應的變量,不讀取的留空。
(, _bool2, ) = returnNamed();

2024.09.25

變量數據存儲

Solidity 的引用類型(Reference Type):數組(array)、結構體(struct),因為這些類型的變量較複雜,占用的儲存空間較大,因此使用時要聲明數據存放的位置。

數據儲存位置:不同位置消耗的 gas 不同。
storage 的數據存在鏈上,類似電腦的硬碟,消耗gas多,反之memorycalldata 消耗的 gas 少。

  • storage:合約中的狀態變量默認都是 storage,儲存在鏈上。
  • memory:函數中的參數和臨時變量,存在記憶體中,不上鏈。
  • calldata:和 memory 類似,不上鏈。但不同點在於 calldata 變量是不可修改的(immutable),通常是函數的參數。 例子:
    function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
        //参数为calldata数组,不能被修改
        // _x[0] = 0 //这样修改会报错
        return(_x);
    }
    

賦值規則

  • 創建了本體的副本,修改新變量不會影響原變量。
  • 創建引用指向本體,修改新變量會影響原變量。
    uint[] x = [1,2,3]; // 状态变量:数组 x
    function fStorage() public{
        //声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x
        uint[] storage xStorage = x;
        xStorage[0] = 100;
    }
    

變量的作用域

  1. 狀態變量:數據存在鏈上的變量,在合約內、函數外宣告,所有合約內函數都可以訪問,gas 消耗高。
contract Variables {
    uint public x = 1;
    uint public y;
    string public z;
}

// 可以在函数里更改状态变量的值
function foo() external{
    x = 5;
    y = 2;
    z = "0xAA";
}
  1. 局部變量:僅在函數執行過程中才有效的變量,局部變量的數據存在記憶體中,不上鏈,gas 低。
function bar() external pure returns(uint){
    uint xx = 1;
    uint yy = 3;
    uint zz = xx + yy;
    return(zz);
}
  1. 全局變量:在全局範圍內工作的變量,都是 solidity 預留的關鍵字,可在不宣告的前提下就使用。 常用的全局變量
function global() external view returns(address, uint, bytes memory){
    address sender = msg.sender; // 請求發起地址
    uint blockNum = block.number; // 當前區塊高度
    bytes memory data = msg.data; // 請求數據
    return(sender, blockNum, data);
}
  1. 全局變量-乙太單位與時間單位:

乙太單位
Solidity 中部存在小數點,以 0 代替小數點以確保交易的精確度損失。

  • wei: 1e9 = 1
  • gwei: 1e9 = 1000000000
  • ether: 1e18 = 1000000000000000000

時間單位
在合約中規定某事件在一定時間後發生,如此可使合約的執行更加精確。

  • seconds: 1
  • minutes: 60 seconds = 60
  • hours: 60 minutes = 3600
  • days: 24 hours = 86400
  • weeks: 7 days = 604800

引用類型

數組 (array)
Solidity 常用的變量型態,儲存一組數據(整數、字節、地址...等)

  • 固定長度 array

    uint[8] array1;
    bytes1[5] array2;
    address[100] array3;
    • memory 修飾的動態數組,可用 new 操作符創建,但必須宣告長度,且宣告後長度不能改變。
      uint[] memory array8 = new uint[](3);
      bytes memory array9 = new bytes(9);
      array8[0] = 1;
      array8[1] = 3;
      array8[2] = 4;
  • 可變長度array

    uint[] array4;
    bytes1[] array5;
    address[] array6;
    bytes array7;

    bytesarray但不用加[],因為bytes為動態大小的字節數組,且它將所有字節緊湊地存儲在一起,這樣可以節省存儲空間,因此bytesbytes1[] 省 gas。

  • method

    • length:取得數組長度,ex.memory 數組的長度固定。
    • push():在數組最後加入 0 元素。
    • push(x):在數組最後加入 x 元素。
    • pop():移除數組最後一個元素。

結構 (struct)
Solidity 可以透過建構struct的形式定義新的類型。struct中的元素可以是原始類型,也可以是引用類型,struct也可以作為數組或映射的元素。

struct Student{
    uint256 id;
    uint256 score; 
}
Student student; // 初始一个student结构体
  • struct賦值的方法:
    • 在函數中創建一個storage的struct引用
    function initStudent1() external{
        Student storage _student = student; // assign a copy of student
        _student.id = 11;
        _student.score = 100;
    }
    • 直接引用狀態變量的struct
    function initStudent2() external{
        student.id = 1;
        student.score = 80;
    }
    • 構造函數式
    function initStudent3() external {
        student = Student(3, 90);
    }
    • key value
    function initStudent4() external {
        student = Student({id: 4, score: 60});
    }

2024.09.26

映射(mapping)類型

在映射中,可以通過鍵(key)來查詢對應的值(value),例如,藉由id查詢姓名。
宣告映射的格式為mapping(KeyType => ValueType),例子:

mapping(uint => address) public idToAddress; // id映射到地址
mapping(address => address) public swapPair; // 币对的映射,地址到地址

映射規則

  1. KeyType只能為 Solidity 內置的值類型,ex.uint, address...,不能用自定義的結構,ex.struct,使用後會報錯。
  2. 映射的數據必須存在storage中,因此可以用於合約的狀態變量,但不能用於public函數的參數或返回結果中,因為mapping紀錄的是關係(key-value pair),且是一種動態的、潛在無限長的結構,無法輕易地序列化或打包為交易的有效負載傳遞。
  3. mapping宣告為public時,Solidity 會自動創建一個getter函數,可以通過key查詢對應的value
  4. mapping新增新的值對:_Var[_Key] = _Value_Var是映射變量名,_Key_Value是對英的鍵值對。
    function writeMap (uint _Key, address _Value) public{
        idToAddress[_Key] = _Value;
    }
    

映射原理

  1. mapping不儲存任何鍵(key)的資訊,也沒有length。
  2. mapping並不直接儲存每個鍵值對,而是使用哈希計算:keccak256(abi.encodePacked(key, slot))當成 offset 存取 value,slot是映射變量定義所在的插槽位置。
  3. Ethereumc會定義所有未使用的空間為0,所以未賦值(value)的鍵(key)初始值都是各個型別的默認值,ex.uint的默認值是0。

變量初始值

在 Solidity 中,宣告但沒賦值的變量都有其初始值。

  • boolean: false
  • string: ""
  • int: 0
  • uint: 0
  • enum: 枚举中的第一个元素
  • address: 0x0000000000000000000000000000000000000000 (或 address(0))
  • function
    • internal: 空白函数
    • external: 空白函数
    bool public _bool; // false
    string public _string; // ""
    int public _int; // 0
    uint public _uint; // 0
    address public _address; // 0x0000000000000000000000000000000000000000
    
    enum ActionSet { Buy, Hold, Sell}
    ActionSet public _enum; // 第1个内容Buy的索引0
    
    function fi() internal{} // internal空白函数
    function fe() external{} // external空白函数 

2024.09.27

常數

  • constant(常量):宣告的時候就必須初始化(需要顯式初始化),之後無法改變。
    uint256 constant CONSTANT_NUM = 10;
  • immutable(不變量):在8.0.21後,immutable不需要顯式初始化(可以使用系統自動分配的默認值)。若immutable在宣告時初始化,且在constructor中再次初始化,會依最後賦予的值為標準,之後無法改變。

※變量不隨意改變的特性可以節省gas,並提升合約的安全性。

題目:
2.下面定义变量的语句中,会报错的一项是:
选择一个答案
A. string constant x5 = "hello world";
B. address constant x6 = address(0);
C. string immutable x7 = "hello world";
D. address immutable x8 = address(0);

ANS:
選項 C 會報錯,因為 immutable 變量不能在聲明時初始化字面值,必須在 構造函數 中初始化。而 constant 變量可以在聲明時初始化字面值。

控制流

  1. if-else
    function ifElseTest(uint256 _number) public pure returns(bool){
        if(_number == 0){
            return(true);
        }else{
            return(false);
        }
    }
  2. for loop
    function forLoopTest() public pure returns(uint256){
        uint sum = 0;
        for(uint i = 0; i < 10; i++){
            sum += i;
        }
        return(sum);
    }
  3. while
    function whileTest() public pure returns(uint256){
        uint sum = 0;
        uint i = 0;
        while(i < 10){
            sum += i;
            i++;
        }
        return(sum);
    }
  4. do while
    function doWhileTest() public pure returns(uint256){
        uint sum = 0;
        uint i = 0;
        do{
            sum += i;
            i++;
        }while(i < 10);
        return(sum);
    }
  5. 三元運算子:Solidity 中唯一一個接受三個操作數的運算符,規則條件? 條件為真的表達式:條件為假的表達式。此運算符經常用作if語句的快捷方式。
    // 三元运算符 ternary/conditional operator
    function ternaryTest(uint256 x, uint256 y) public pure returns(uint256){
        // return the max of x and y
        return x >= y ? x: y; 
    }
  6. conitnue:立刻進入下輪循環。
  7. break:跳出當前循環。

插入排序

    // 插入排序 错误版
function insertionSortWrong(uint[] memory a) public pure returns(uint[] memory) {    
    for (uint i = 1;i < a.length;i++){
        uint temp = a[i];
        uint j=i-1;
        while( (j >= 0) && (temp < a[j])){
            a[j+1] = a[j];
            j--;
        }
        a[j+1] = temp;
    }
    return(a);
}

因為Solidity使用的uint只能為正整數,若取到負值則會有underflow錯誤,在以上的程式中j有可能取到-1

// 插入排序 正确版
function insertionSort(uint[] memory a) public pure returns(uint[] memory) {
    // note that uint can not take negative value
    for (uint i = 1;i < a.length;i++){
        uint temp = a[i];
        uint j=i;
        while( (j >= 1) && (temp < a[j-1])){
            a[j] = a[j-1];
            j--;
        }
        a[j] = temp;
    }
    return(a);
}

2024.09.28

構造函數和修飾器

以合約權限(Ownable)的例子介紹。

  • constructor構造函數:每個合約可以定義一個,在部屬合約後會自動運行一次,可以用來初始化合約的參數。
address owner; // 定义owner变量
// 构造函数
constructor(address initialOwner) {
    owner = initialOwner; // 在部署合约的时候,将owner设置为传入的initialOwner地址
}

※在0.4.22前,構造函數命名為與合約同名。

  • modifier修飾器:Solidity特有的語法,主要使用在運行函數前的檢查,ex.地址、變量、餘額等...。
// 定义modifier
modifier onlyOwner {
   require(msg.sender == owner); // 检查调用者是否为owner地址
   _; // 如果是的话,继续运行函数主体;否则报错并revert交易
}

圖中示意:以 owner 地址的身分呼叫changeOwner,交易成功。

OpenZeppelin是維護標準化程式庫的組織。

事件

Solidity 的事件是EVM(乙太坊虛擬機)上的日誌抽象 以轉帳ERC20為例。

  • 特點:

    • 響應:應用程式ether.js可以通過RPC接口訂閱及監聽事件,而實現與區塊鏈的互動。當合約觸發事件時,前端應用程式可以根據事件的數據做出相應的響應。
      ex.觸發轉帳成功,前端捕捉到此事件後,告知用戶已轉帳成功。
    • 經濟:eventEVM上相對經濟的存儲方式。觸發一個事件大概消耗2000 gas,而在區塊鏈上儲存一個新的變量至少需要20000 gas,因此event可以有效節省資源。
      • 事件的數據會存儲在 交易日誌 中,這些日誌儲存在區塊鏈上,但不會被合約內部直接存取,只能通過外部工具查詢和訂閱。
      • 日誌存儲在區塊鏈的交易日誌部分,而不是持久化存儲中,這使得它的操作更經濟。
  • 宣告方式:用event宣告,接者加上事件名稱。 ex.

    event Transfer(address indexed from, address indexed to, uint256 value);

    以上程式中,共紀錄了3個變量fromtovalue,分別對應代幣的轉帳地址、接收地址、轉帳數量...
    此外,fromto前面帶有indexed關鍵字,會保存到虛擬日誌的topics中,以便日後檢索。

  • 釋放事件:在函數中釋放事件。
    以下例子中,每次用_transfer()函數轉帳時都會釋放Transfer事件,並記錄相應變量。

    // 定义_transfer函数,执行转账逻辑
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) external {
    
        _balances[from] = 10000000; // 给转账地址一些初始代币
    
        _balances[from] -=  amount; // from地址减去转账数量
        _balances[to] += amount; // to地址加上转账数量
    
        // 释放事件
        emit Transfer(from, to, amount);
    }

EVM日誌(log):

EVM使用log儲存Solidity事件,每條日誌都包含主題topics與數據data兩部分。

  • topics:log的第一部分是主題數組,用於標識事件類型,長度不能超過4,第一個元素是事件的hash,對於上例的Transfer事件:

    keccak256("Transfer(address,address,uint256)")
    
    //0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

    除了hash主題還可以包含最多3個indexed參數,也是Transfer事件中的fromto

    • 檢索效率:indexed 參數用作主題的索引,方便區塊鏈上的事件查詢。通過將參數標記為 indexed,可以將其設置為檢索鍵,這樣可以過濾事件,只返回指定參數值的事件。
    • 固定大小限制:每個 indexed 參數的大小固定為 256 bytes。如果參數過大,例如字符串或數組,Solidity 會自動將它們哈希化後存儲在主題中。
  • data:這是事件觸發時記錄的具體數據,包括事件的非索引參數。這些數據可以在交易日誌中找到。

    • 事件中不帶indexed的參數會被存在data中,這部分的變量不能被直接檢索但可以儲存任意大小的數據。
    • data這部分的變量在儲存上消耗的gastopics更少。

2024.09.29

繼承

  • 規則:

    • virtual:父合約中的函數,在子合約中若需要重寫,要加上virtual關鍵字。
    • override:若在子合約中重寫了父合約中的函數,則需要加上override關鍵字。 ※若override修飾public變量,則會重寫與變量同名的getter函數。
      mapping(address => uint256) public override balanceOf;
  • 簡單繼承

  • 多重繼承:

  • 修飾器的繼承:

  • 構造函數的繼承:

  • 調用父合約:

    1. 直接調用:直接用父合約名.函數名()的方式調用父合約函數。
    2. super關鍵字:子合約可以利用super.函數名()調用最近的父合約函數。
      以多重繼承下方的圖片為例,super.pop()將呼叫Baba.pop()
  • 鑽石(菱形)繼承:

    /* 继承树:
    God
    /  \
    Adam Eve
    \  /
    people
    */
    

抽象(abstract)合約

合約中若至少有一個未實現的函數,即某個函數缺少主體{}中的內容,則此合約必須標為abstract,且未實現的函數須加上virtual,以便子合約重寫。
ex.

abstract contract InsertionSort{
    function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);
}

2024.09.30

接口(interface)

類似抽象合約,但不實現任何功能。

  • 規則:
    1. 不能包含狀態變量
    2. 不能包含構造函數
    3. 不能繼承接口以外的其他合約
    4. 所有函數都必須是external且不能有函數體
    5. 繼承 interface 的非抽象合約必須實現 interface 定義的所有功能

interface 是智能合約的骨架,定義了合約的功能及如何觸發,若合約實現了某種接口,ex.ERC20ERC721,其他Dapps和合約就知道該如何與此智能合約交互。

  • 作用:
    1. 標準化交互:
      如果某個智能合約實現了一個通用的接口(如 ERC20 或 ERC721),那麼其他合約或應用程序就能根據這個接口來與其互動,有助於跨平台、跨合約之間的兼容性。
    2. 定義合約中的功能:
      接口會定義每個函數的名稱和參數類型,以及這些函數如何被調用,這相當於描述了合約的公共 API。
    3. 定義每個函數哈希(bytes4選擇器)、函數簽名(函數名(每個參數類型)),以通過對函數的簽名進行哈希後得到獨特的4字節哈希。
      keccak256("transfer(address,uint256)").slice(0, 4)
    4. 接口 ID:
      接口 ID 是一個唯一的標識符,用來表示某個合約是否實現了某個接口。
interface IERC721 is IERC165 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    
    function balanceOf(address owner) external view returns (uint256 balance);

    function ownerOf(uint256 tokenId) external view returns (address owner);

    function safeTransferFrom(address from, address to, uint256 tokenId) external;

    function transferFrom(address from, address to, uint256 tokenId) external;

    function approve(address to, uint256 tokenId) external;

    function getApproved(uint256 tokenId) external view returns (address operator);

    function setApprovalForAll(address operator, bool _approved) external;

    function isApprovedForAll(address owner, address operator) external view returns (bool);

    function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external;
}

什麼時候使用interface?

若知道一個合約實現了IERC721,不需要知道他的具體程式實現就可以與之交互。
ex.

contract interactBAYC {
    // 利用BAYC地址创建接口合约变量(ETH主网)
    IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);

    // 通过接口调用BAYC的balanceOf()查询持仓量
    function balanceOfBAYC(address owner) external view returns (uint256 balance){
        return BAYC.balanceOf(owner);
    }

    // 通过接口调用BAYC的safeTransferFrom()安全转账
    function safeTransferFromBAYC(address from, address to, uint256 tokenId) external{
        BAYC.safeTransferFrom(from, to, tokenId);
    }
}

異常

異常命令可以幫助尋找錯誤。

  • error:可以在contract之外拋出異常,高效且方便的向用戶解釋操作失敗的原因,且在拋出異常時可攜帶參數。
    ex.定義TransferNotOwner異常,當用戶不是貨幣owner時轉帳會拋出錯誤。
    error TransferNotOwner(); // 自定义error
    
    // 攜帶參數的異常,以提示轉帳的帳戶地址
    error TransferNotOwner(address sender); // 自定义的带参数的error
    error需搭配revert命令使用:
    當用戶不是貨幣owner時轉帳會拋出錯誤,否則成功轉帳。
    function transferOwner1(uint256 tokenId, address newOwner) public {
        if(_owners[tokenId] != msg.sender){
            revert TransferNotOwner();
            // revert TransferNotOwner(msg.sender);
        }
        _owners[tokenId] = newOwner;
    }
  • require:消耗的gas比error高,且會隨著描述異常的字符串長度增加,gas也隨之增加。
    • 使用方法:require(檢查條件,"異常的描述"),檢查條件不成立時,拋出異常。
    function transferOwner2(uint256 tokenId, address newOwner) public {
        require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
        _owners[tokenId] = newOwner;
    }
  • assert:不能解釋拋出異常的原因(相較require少了字符串),但仍會在檢查條件不成立時拋出異常。
    function transferOwner3(uint256 tokenId, address newOwner) public {
        assert(_owners[tokenId] == msg.sender);
        _owners[tokenId] = newOwner;
    }

驗證

輸入任意數字、非0地址,呼叫以各種語法寫的異常訊息。


  • error方法gas消耗:24457(加入參數後gas消耗:24660)
  • require方法gas消耗:24755
  • assert方法gas消耗:24473
    error方法gas消耗最少,require方法消耗最多,因此在求最小gas消耗下可以多加使用error

2024.10.01

重載overload

重載意即名稱相同但輸入參數類型不同的函數可以同時存在,且視為不同的函數。

function saySomething() public pure returns(string memory){
    return("Nothing");
}

function saySomething(string memory something) public pure returns(string memory){
    return(something);
}

實參匹配Argument Matching

在呼叫overload函數時,會把輸入的實際參數和函數參數的變量做匹配。若出現多個匹配的重載函數,會報錯。
ex.若呼叫f(),且傳入50,因為50可以被轉換為uint8,也可以被轉換為uint256,因此會報錯。

function f(uint8 _in) public pure returns (uint8 out) {
    out = _in;
}

function f(uint256 _in) public pure returns (uint256 out) {
    out = _in;
}

Solidity中是否允许修饰器(modifier)> 重载?
选择一个答案
A. 允许 B. 不允许

ANS:B. 不允许
解釋: Solidity 中不允許修飾器(modifier)重載。修飾器是用來修改函數行為的一段代碼邏輯,它不能像函數那樣通過不同的參數來進行重載。每個修飾器必須有唯一的名稱,且不能有相同名稱但不同參數的多個修飾器。

下面两个函数的函数选择器是否相同?

function f(uint8 _in) public pure returns (uint8 out) { 
out = _in; 
} 

function f(uint256 _in) public pure returns (uint256 out) { 
out = _in; 
}

A. 相同
B. 不相同
ANS:B

庫合約

為一種特殊的合約,目的是為了提升程式的復用性和減少gas。庫合約是一系列的函數合集。

  • 相較普通合約的特殊點:
    1. 不能存在狀態變量
    2. 不能繼承或被繼承
    3. 不能接收以太幣
    4. 不可以被銷毀

庫合約中函數的可見性若被設為publicexternal,則在呼叫函數時會觸發一次delegatecall。設為internal,則不會引起。設為private的函數僅能在庫合約中可見。

Strings庫合約

Strings庫合約是將uint256類型轉換為string類型的程式庫。

ex.以下的程式主要包含兩個函數,toString()uint256轉換為stringtoHexString()uint256轉換為16進制,再轉換為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. 利用using for指令:
    using A for B;,用於附加合約(從庫A)到任何類型(B)。執行完畢後,庫A中的函數會自動添加為B類型變量的成員,並可以直接呼叫。
// 利用using for指令
using Strings for uint256;
function getString1(uint256 _number) public pure returns(string memory){
    // 库合约中的函数会自动添加为uint256型变量的成员
    return _number.toHexString();
}
  1. 通過庫合約名稱呼叫函數:
// 直接通过库合约名调用
function getString2(uint256 _number) public pure returns(string memory){
    return Strings.toHexString(_number);
}

Q:库合约和普通合约的区别,下列描述错误的是:

A. 库合约不能存在状态变量
B. 库合约不能继承
C. 库合约可以被继承
D. 库合约不能被销毁

A:C,庫合約不能被繼承,這使得它與普通合約不同。

常用庫合約

  • Strings:將uint256轉換為String
  • Address:判斷某個地址是否為合約的地址。
  • Create2:更安全的使用Create2 EVM opcode
  • Arrays:跟數組相關的庫合約。

2024.10.02

import

import可以用來在一個文件中引用另一個文件的內容,提高程式的可重用性、組織性。

用法:

  • 通過源文件相對位置導入:
    文件结构
    ├── Import.sol
    └── Yeye.sol
    
    // 通过文件相对位置import
    import './Yeye.sol';
  • 通過源文件網址導入網上合約的全局符號:
    // 通过网址引用
    import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
  • 通過npm的目錄導入:
    import '@openzeppelin/contracts/access/Ownable.sol';
  • 通過指定全局符號導入合約特定的全局符號。
    import {Yeye} from './Yeye.sol';

Q:Solidity中import的作用是:

A. 导入其他合约中的接口
B. 导入其他合约中的私有变量
C. 导入其他合约中的全局符号
D. 导入其他合约中的内部变量

Ans:import 可以導入所有全局可用的符號(如函數、結構、事件等),這是最全面的描述。

Q:import导入文件中的全局符号可以单独指定其中的:

A. 合约
B. 纯函数
C. 结构体类型
D. 以上都可以

Ans:
A. 合約:可以單獨導入合約,例如 import {Yeye} from "./Yeye.sol";。

B. 纯函数:可以導入純函數(如果存在),例如 import {someFunction} from "./SomeLib.sol";。

C. 结构体类型:結構體類型也可以單獨導入,例如 import {SomeStruct} from "./SomeStruct.sol";。

Q:被导入文件中的全局符号想要被其他合约单独导入,应该怎么编写?

A. 将合约结构包含
B. 包含在合约结构中
C. 与合约并列在文件结构中

Ans:在 Solidity 中,如果你想要導入某個文件中的全局符號(例如合約、函數、結構體等),這些符號必須在文件的最外層與合約並列定義,而不是在合約內部。這樣才能被其他合約單獨導入。

2024.10.03

接收ETH

Solidity支持兩種特殊的回調函數,receive()fallback()

  • 使用情況:
    1. 接收ETH
    2. 處理合約中不存在的函數調用 (代理合約proxy contract)

※ 在Solidity 0.6.x版本之前,只有fallback()函數,0.6版本之後fallback()函数才被拆分成receive()fallback()

receive()

  • 用途:receive() 函數在合約收到 ETH 轉帳時被呼叫的函數。

  • 特點:

    • 只能有一個 receive() 函數
    • 宣告時不用function關鍵字
    • 不接受任何參數,並且沒有返回值,且必須包含externalpayable
    • 合約接收 ETH 的時候,receive()會被觸發,函數中不能執行太多邏輯,因為當用sendtransfer方法發送 ETH 時,gas 會被限制在2300,此時receive()太複雜會觸發Out of Gas報錯;如果用call就可以自定義gas以執行更複雜的邏輯。
    // 定义事件
    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

  • 特點:

    • 宣告時不用function關鍵字
    • 必須由externalpayable修飾,用於接收ETH:fallback() external payable { ... }。 event fallbackCalled(address Sender, uint Value, bytes Data);

    ex.以下程式,觸發時會釋放fallbackCalled事件,並輸出msg.sendermsg.valuemsg.data

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

receivefallback的區別
receivefallback都能用於接收 ETH,觸發規則如下:

触发fallback() 还是 receive()?
           接收ETH
              |
         msg.data是空?
            /  \
          是    否
          /      \
receive()存在?   fallback()
        / \
       是  否
      /     \
receive()   fallback()

※ 合約接收到 ETH 時,msg.data為空且存在receive()時,會觸發receive()msg.data不為空或不存在receive()時,會觸發fallback(),此時fallback()必須為payable

receive()payable fallback()均不存在時,向合約直接發送 ETH 將會報錯(但仍可以通過帶有payable的函數向合約發送 ETH)。

2024.10.04

發送 ETH

Solidity 中向其他合約發送ETH的方法共有三種:transfer()send()call()(最鼓勵使用)。

接收 ETH 合約
部署一個接收ETH的合約 ReceiveETH,其中有一個事件Log,事件會記錄收到的ETH數量和gas剩餘;兩個函數(1)receive(),收到ETH後被觸發,(2)getBalance(),查詢合約的ETH餘額。

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) {
        retuen address(this).balance;
    }
}

發送 ETH 合約
實作三種方法向ReceiveETH合約發送ETH。先在發送ETH合約的SendETH中實現payableconstructorreceive(),使我們能在部署時和部署後向合約轉帳。

contract SendETH{
    // 構造函數,payable使的部署的時向合約發送eth
    constructor() payable{}
    // receuve方法,接收eth時被觸發
    receive() external payable{}
}

transfer

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

send

  • 接收方地址.send(發送ETH數額)
  • send()gas限制是2300,足夠用於轉帳,但對方合約的fallback()receive()不能實作太複雜的邏輯。
  • send()如果轉帳失敗,不會自動revert
  • send()的返回值是bool,代表轉帳成功或失敗。
error SendFailed(); // 用send發送ETH失敗

// 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限制,可支持對方合約實作複雜邏輯。
  • call()如果轉帳失敗,不會自動revert
  • call()的返回值是(bool, bytes)bool代表轉帳成功或失敗。
error CallFailed(); // 用call發送ETH失敗

// call()發送ETH
function callETH(address payable _to, uint256 amount) external payable{
    // 處理call的返回值,如果失敗,revert交易並發送error
    (bool success,) = _to.call{value: amount}("");
    if(!success){
        revert CallFailed();
    }
}

2024.10.05

調用已部署合約

Solidity 中,一個合約可以調用另一個合約的函數,在建構 DAPP 時非常有用。

目標合約
假設現在有一個簡單的合約OtherContract,用於被其他合約調用。
其中包含狀態變量_x,事件Log在收到ETH時觸發,三個函數:getBalance()seX()getX()

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;
    }
}
  1. 傳入合約地址
    在函數中傳入目標合約地址,生成目標合約的引用,然後調用目標函數。

    function callSetX(address _Address, uint256 x) external{
        OtherContract(_Address).setX(x);
    }
  2. 傳入合約變量
    直接在函數裡傳入合約的引用。

    function callGetX(OtherContract _Address) external view returns(uint x){
        x = _Address.getX();
    }
  3. 創建合約變量
    創建新合約變量,並通過合約變量來調用目標函數。

    function callGetX2(address _Address) external view returns(uint x){
        OtherContract oc = OtherContract(_Address);
        x = oc.getX();
    }
  4. 調用合約並發送ETH
    如果目標合約的函數是payable的,可通過調用其來給合約轉帳。
    _Name(_Address).f{value: _Value}(),其中_Name是合约名,_Address是合约地址,f是目标函数名,_Value是要转的ETH数额(以wei为单位)。

    function setXTransferETH(address otherContract, uint256 x) payable external{
        OtherContract(otherContract).setX{value: msg.value}(x);
    }

Q:假设我们部署了合约 OtherContract (合约内容见下)
其合约地址为 0xd9145CCE52D386f254917e481eB44e9943F39138。我们希望在另一个合约中调用该合约,考虑如下两种方式:

//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;
    }
}
(1) OtherContract other = OtherContract(0xd9145CCE52D386f254917e481eB44e9943F39138)
(2) IOtherContract other = IOtherContract(0xd9145CCE52D386f254917e481eB44e9943F39138)

下列说法正确的是:
A. (1)(2) 两种写法均会报错
B. 仅 (1) 是调用其他合约的正确写法,(2) 会报错
C. 仅 (2) 是调用其他合约的正确写法,(1) 会报错
D. (1)(2) 均是调用其他合约的正确写法

Ans:D
(1) 直接实例化合约 OtherContract:这是通过已知的合约地址实例化一个合约对象。这种方式在合约内部执行的操作和直接在其他合约中执行是等价的,因为合约 OtherContract 已经被部署,并且已知其地址。

(2) 通过接口 IOtherContract 实例化:这是一种更灵活的方式,通过接口可以在合约的不同版本之间更轻松地进行交互。只要目标合约实现了 IOtherContract 接口,便可以通过该接口的方式进行调用。这也同样是正确的调用方法。

2024.10.06

Call

calladdress類型的低級成員函數,運行時動態調用其他合約的函數。返回值為(bool, bytes memory),分别對應call是否成功以及目標函數的返回值。

  • Gas:調用者可以控制調用時提供的 gas,多餘的 gas 會退還给調用者。
  • call是solidity官方推薦的通過觸發fallbackreceive函數發送ETH的方法。
  • 不建議用call調用另一個合約,因為當調用的是不安全合約的函數時,就等於將主動權交給了它。建議使用先宣告合約變量再調用函數的方式。
  • 當不知道對方合約的源代碼或者ABI時,就沒辦法生成合約變量,此時仍可以通過call調用對方合約的函數。

call使用規則

目标合约地址.call(字节码);

其中字節碼利用結構化編碼函數abi.encodeWithSignature獲得:

// abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)

(bool success, bytes memory data) = targetContract.call(
    abi.encodeWithSignature("someFunction(uint256)", 123)
);

函數簽名"函數名(逗號分隔的參數類型)"。例如abi.encodeWithSignature("f(uint256,address)", _x, _addr)

另外call在調用合約時可以指定交易發送的ETH數額和gas數額:

目标合约地址.call{value:发送数额, gas:gas数额}(字节码);

目標合約 與上章的OtherContract基本相同,但多了fallback函數。

contract OtherContract {
    uint256 private _x = 0; // 状态变量x
    // 收到eth的事件,记录amount和gas
    event Log(uint amount, uint gas);
    
    fallback() external payable{}

    // 返回合约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;
    }
}

利用call調用目標合約

1.Response事件 我們寫一個call合約來呼叫目標合約的函數,首先定義一個Response事件,輸出call返回的successdata,以觀察返回值。

// 定义Response事件,输出call返回的结果success和data
event Response(bool success, bytes data);

2.調用setX函數 定義callSetX函數去調用目標合約的setX(),轉入msg.value數額的ETH,並釋放Response事件輸出successdata:

function callSetX(address payable _addr, uint256 x) public payable {
    // call setX(),同时可以发送ETH
    (bool success, bytes memory data) = _addr.call{value: msg.value}(
        abi.encodeWithSignature("setX(uint256)", x)
    );

    emit Response(success, data); //释放事件
}

接著調用callSetX把狀態變量_x改為5,參數為OtherContract地址和5,由於目標函數setX()沒有返回值,因此Response事件輸出的data0x,意即空。

3.調用getX函數

調用getX函數,將返回目標合約_x的值,類型為uint256。可以用abi.decode解碼call的返回值data,並輸出數值。

function callGetX(address _addr) external returns(uint256){
    // call getX()
    (bool success, bytes memory data) = _addr.call(
        abi.encodeWithSignature("getX()")
    );

    emit Response(success, data); //释放事件
    return abi.decode(data, (uint256));
}

4.調用不存在的函數 如果輸入call的目標函數不存在目標合約,那麼目標合約的fallback被觸發。

function callNonExist(address _addr) external{
    // call 不存在的函数
    (bool success, bytes memory data) = _addr.call(
        abi.encodeWithSignature("foo(uint256)")
    );

    emit Response(success, data); //释放事件
}

2024.10.07

Delegatecall

delegatecallcall類似,是 Solidity 中地址類型的低級成員函數。與cal的區別是會在調用者合約的上下文中執行被調用合約的程式。delegate意即委託

當用戶A通過合約Bcall合約C的時候,執行的是合約C的函數,上下文(content,理解為包含變量和狀態的環境)是合約c的。

當用戶A通過合約Bdelegatecall合約C的時候,執行的是合約C的函數,上下文是合約b的,且若函數改變一些狀態變量,產生的效果會作用在合約B的變量上。
想像成:投資者(A)將資產(B)交給風險投資者(C)。執行者是C,但改變的是B。

語法

目标合约地址.delegatecall(二进制编码);

其中二进制编码利用结构化编码函数abi.encodeWithSignature获得:

abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)

函数签名"函数名(逗号分隔的参数类型)"。例如abi.encodeWithSignature("f(uint256,address)", _x, _addr)
call不一樣,delegatecall在調用合約時可以指定教意發送的gas,但不能指定發送的ETH數額。

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

什麼情況下會用到delegatecall?

  1. 代理合約(Proxy Contract):將智能合約的儲存合約跟邏輯合約(Logic Contract)分開,Proxy Contract存儲所有相關變量,並保存邏輯合約的地址;所有函數存在邏輯合約中,藉由delegatecall執行。當升級時,只需要將代理合約指向新的邏輯合約。
  2. EIP-2535 Diamonds(鑽石):鑽石是一個支持構建可在生產中擴展的模塊化智能合約系統的標準。鑽石是具有多個實施合約的代理合約。
    鑽石標準簡介

delegatecall例子

結構:你(A)通過合約B調用目標合約C。

  • 被調用的合約C
    先寫一個簡單的目標合約C:
    兩個public變量:uint numaddress sender
    函數setVars(uint _num),可將num設定為傳入的_num,並將sender設為msg.sender
// 被调用的合约C
contract C {
    uint public num;
    address public sender;

    function setVars(uint _num) public payable {
        num = _num;
        sender = msg.sender;
    }
}
  • 發起調用的合約B
    合約B與目標合約C的變量存儲布局必須相同,兩個變量順序需要相同(先numsender,變量名稱可以不同)。
contract B {
    uint public num;
    address public sender;
}
// 通过call来调用C的setVars()函数,将改变合约C里的状态变量
// _addr 對應合約c的地址
// _num 對應合約c的參數
function callSetVars(address _addr, uint _num) external payable{
    // call setVars()
    (bool success, bytes memory data) = _addr.call(
        abi.encodeWithSignature("setVars(uint256)", _num)
    );
}
// 通过delegatecall来调用C的setVars()函数,将改变合约B里的状态变量
function delegatecallSetVars(address _addr, uint _num) external payable{
    // delegatecall setVars()
    (bool success, bytes memory data) = _addr.delegatecall(
        abi.encodeWithSignature("setVars(uint256)", _num)
    );
}

call vs delegatecall 的主要區別

特性 call delegatecall
狀態變量上下文 目標合約(C)的狀態變量 調用者合約(B)的狀態變量
msg.sender 目標合約的 msg.sender 調用者合約的 msg.sender
msg.value 目標合約接收到的 msg.value 調用者合約接收到的 msg.value
存儲修改 修改目標合約的存储 修改調用者合約的存储
適用場景 常規的合約調用、發送 ETH 代理合約模式、庫合約復用
函數執行上下文 在目標合約中執行 在調用者合約的上下文中執行

2.当用户A通过合约B来delegatecall合约C时,执行了__的函数,语境是__,msg.sender和msg.value来自__, 并且如果函数改变一些状态变量,产生的效果会作用于__的变量上。
A. C;B;A;B
B. C;C;B;C
C. B;B;A;B
D. C;B;A;C

Ans:A

  • 執行了合約 C 的函數,因為 delegatecall 是調用合約 C 的函數代碼。
  • 語境 是合約 B,這是因為 delegatecall 繼承了合約 B 的上下文,即使用合約 B 的存儲空間和狀態變量。
  • msg.sender 和 msg.value 來自用戶 A,這些參數在 delegatecall 中保持不變。
  • 如果函數改變了一些狀態變量,這些改變會作用於合約 B 的變量上,因為 delegatecall 使用了合約 B 的存儲。

3.delegatecall在调用合约时__________________________

A. 可以指定交易发送的gas,也可以指定发送的ETH数额
B. 可以指定交易发送的gas,但不可以指定发送的ETH数额
C. 不可以指定交易发送的gas,也不可以指定发送的ETH数额
D. 不可以指定交易发送的gas,但可以指定发送的ETH数额

Ans:B

  • 它可以指定交易發送的 gas,因為 delegatecall 的語法允許透過 {gas: xxx} 來設定 gas 限制。
  • 但 delegatecall 不能 發送 ETH,因為它僅傳遞執行上下文,不會攜帶任何資金轉移。

6.在代理合约中,存储所有相关的变量的是___,存储所有函数的是___,同时____________

A. 代理合约; 逻辑合约; 代理合约delegatecall逻辑合约
B. 代理合约; 逻辑合约; 逻辑合约delegatecall代理合约
C. 逻辑合约; 代理合约; 代理合约delegatecall逻辑合约
D. 逻辑合约; 代理合约; 逻辑合约delegatecall代理合约

Ans:A

  • 代理合約(Proxy Contract)存儲所有相關的狀態變量,因為所有狀態的變更都會發生在代理合約的存儲空間。
  • 邏輯合約(Logic Contract)存儲所有函數,實際的邏輯運行是在邏輯合約中定義的函數中完成的。
  • 代理合約使用 delegatecall 呼叫邏輯合約來執行函數,這樣變更會影響代理合約的存儲。

2024.10.08

在合約中創建新合約

在乙太坊鏈上,用戶(外部帳戶,EOA)和智能合約都具備創建新的智能合約的能力。這種功能的實現使的合約之間可以互相交互、組合,並實現更複雜的去中心化應用(DApps)。
中心化交易所uniswap就是利用工廠合約(PairFactory)創建和管理無數個交易對合約(Pair Contract),每個交易對合約代表一個特定的代幣對(如 ETH/DAI)。

create
有兩種方法可以在合約中創建新合約,createcreate2
create的用法很簡單,就是new一個合約,並傳入新合約構造函數所需的參數:
Contract是要創建的合約名,x是合約對象(地址),如果構造函數是payable,可以創建時傳入_value數量的ETHparams是新合約構造函數的參數。

Contract x = new Contract{value: _value}(params)

極簡Uniswap

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

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

以下用create方法實現簡易版的Uniswap

Pair合約

contract Pair{
    address public factory; // 工厂合约地址
    address public token0; // 代币1
    address public token1; // 代币2

    constructor() payable {
        factory = msg.sender;
    }

    // called once by the factory at time of deployment
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
        token0 = _token0;
        token1 = _token1;
    }
}

Pair合約很簡單,包含3個狀態變量:factory, token0token1
構造函數construct在部署時將factory賦值為工廠合約的地址。initialize函數會由工廠合約在部署完成後手動調用已初始化代幣地址,將tokentoken1更新為幣對中兩種代幣的地址。

为什么uniswap不在constructor中将token0和token1地址更新好?

因为uniswap使用的是create2创建合约,生成的合约地址可以实现预测,更多详情请阅读第25讲。

PairFactory

contract PairFactory{
    mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
    address[] public allPairs; // 保存所有Pair地址

    function createPair(address tokenA, address tokenB) external returns (address pairAddr) {
        // 创建新合约
        Pair pair = new Pair(); 
        // 调用新合约的initialize方法
        pair.initialize(tokenA, tokenB);
        // 更新地址map
        pairAddr = address(pair);
        allPairs.push(pairAddr);
        getPair[tokenA][tokenB] = pairAddr;
        getPair[tokenB][tokenA] = pairAddr;
    }
}

工廠合約(PairFactory)有兩個狀態變量getPair是兩個代幣地址到幣對地址的map,方便根據代幣找到幣對地址。

PairFactory合約只有一個createPair函數,根據輸入的兩個代幣地址tokenATokenB來創建新的Pair合約。
以下為創建合約的程式:

Pair pair = new Pair(); 
WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC链上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c