Skip to content

Latest commit

 

History

History
1904 lines (1697 loc) · 79.1 KB

Pearl.md

File metadata and controls

1904 lines (1697 loc) · 79.1 KB

~~--- timezone: Asia/Shanghai

YourName

  1. 自我介绍 Pearl, female.

  2. 你认为你会完成本次残酷学习吗? Probably

Notes

2024.09.23

學習內容:

值類型

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract HelloWeb3{
    string public _string = "Hello Web3!";
    // 布尔值
    bool public _bool = true;
    // 布尔运算
    bool public _bool1 = !_bool; // 取非
    bool public _bool2 = _bool && _bool1; // 与
    bool public _bool3 = _bool || _bool1; // 或
    bool public _bool4 = _bool == _bool1; // 相等
    bool public _bool5 = _bool != _bool1; // 不相等
    // && 和 || 运算符遵循短路规则


    // 整型
    int public _int = -1; // 整数,包括负数
    uint public _uint = 1; // 正整数
    uint256 public _number = 20220330; // 256位正整数
    // 整数运算
    uint256 public _number1 = _number + 1; // +,-,*,/
    uint256 public _number2 = 2**2; // 指数
    uint256 public _number3 = 7 % 2; // 取余数
    bool public _numberbool = _number2 > _number3; // 比大小


    // 地址
    address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
    address payable public _address1 = payable(_address); // payable address,可以转账、查余额
    // 地址类型的成员
    uint256 public balance = _address1.balance; // balance of address


    // 固定长度的字节数组
    bytes32 public _byte32 = "MiniSolidity"; 
    bytes1 public _byte = _byte32[0]; 


    // 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
    enum ActionSet { Buy, Hold, Sell }
    // 创建enum变量 action
    ActionSet action = ActionSet.Buy;
    // enum可以和uint显式的转换
    function enumToUint() external view returns(uint){
        return uint(action);
    }
}

函數

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract FunctionTypes{
    uint256 public number = 5;
    // 默认function
    function add() external{
        number = number + 1;
    }

    // pure: 纯纯牛马
    function addPure(uint256 _number) external pure returns(uint256 new_number){
        new_number = _number + 1;
    }
    // view: 看客
    function addView() external view returns(uint256 new_number) {
        new_number = number + 1;
    }

    // 返回多个变量
    function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
        return(1, true, [uint256(1),2,5]);
    }
    // 命名式返回,也可以用 return 来返回变量
    function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
        _number = 2;
        _bool = false;
        _array = [uint256(3),2,1];
    }

    function read() public pure {
        uint256 _number;
        bool _bool;
        bool _bool2;
        uint256[3] memory _array;

        // 读取所有返回值
        (_number, _bool, _array) = returnNamed();

        // 只读取_bool,而不读取返回的_number和_array
        (, _bool2, ) = returnNamed();
    }
}

2024.09.24

引用類型(Reference Type)

  • 数据位置和赋值规则
contract referenceType{
    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;
    }
}
  • 变量的作用域
contract Variables {
    // state variables
    uint public x = 1;
    uint public y;
    string public z;
    function foo() external{
        // 可以在函数里更改状态变量的值
        x = 5;
        y = 2;
        z = "0xAA";
    }

    function bar() external pure returns(uint){
        // local variables
        uint xx = 1;
        uint yy = 3;
        uint zz = xx + yy;
        return(zz);
    }

    function global() external view returns(address, uint, bytes memory){
        // global variables
        address sender = msg.sender;
        uint blockNum = block.number;
        bytes memory data = msg.data;
        return(sender, blockNum, data);
    }

    // 以太单位
    function weiUnit() external pure returns(uint) {
        assert(1 wei == 1e0);
        assert(1 wei == 1);
        return 1 wei;
    }

    function gweiUnit() external pure returns(uint) {
        assert(1 gwei == 1e9);
        assert(1 gwei == 1000000000);
        return 1 gwei;
    }

    function etherUnit() external pure returns(uint) {
        assert(1 ether == 1e18);
        assert(1 ether == 1000000000000000000);
        return 1 ether;
    }

    // 时间单位
    function secondsUnit() external pure returns(uint) {
        assert(1 seconds == 1);
        return 1 seconds;
    }

    function minutesUnit() external pure returns(uint) {
        assert(1 minutes == 60);
        assert(1 minutes == 60 seconds);
        return 1 minutes;
    }

    function hoursUnit() external pure returns(uint) {
        assert(1 hours == 3600);
        assert(1 hours == 60 minutes);
        return 1 hours;
    }

    function daysUnit() external pure returns(uint) {
        assert(1 days == 86400);
        assert(1 days == 24 hours);
        return 1 days;
    }

    function weeksUnit() external pure returns(uint) {
        assert(1 weeks == 604800);
        assert(1 weeks == 7 days);
        return 1 weeks;
    }
}

数组 array

  • 固定长度数组:在声明时指定数组的长度。用T[k]的格式声明,其中T是元素的类型,k是长度
  • 可变长度数组(动态数组):在声明时不指定数组的长度。用T[]的格式声明,其中T是元素的类型
contract arrayType{
    // 固定长度 Array
    uint[8] array1;
    bytes1[5] array2;
    address[100] array3;
    // 可变长度 Array
    uint[] array4;
    bytes1[] array5;
    address[] array6;
    bytes array7;
}
  • memory修饰的动态数组:可以用new操作符来创建,但是必须声明长度,并且声明后长度不能改变
  • 如果创建的是动态数组,你需要一个一个元素的赋值。
contract memoryArray{
    // memory动态数组
    uint[] memory array8 = new uint[](5);
    bytes memory array9 = new bytes(9);
}
  • 数组成员
    • length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。
    • push(): 动态数组拥有push()成员,可以在数组最后添加一个0元素,并返回该元素的引用。
    • push(x): 动态数组拥有push(x)成员,可以在数组最后添加一个x元素。
    • pop(): 动态数组拥有pop()成员,可以移除数组最后一个元素。 结构体 struct
  • 结构体中的元素可以是原始类型,也可以是引用类型;结构体可以作为数组或映射的元素。
contract structType{
   // 结构体
   struct Student{
       uint256 id;
       uint256 score; 
   }

   Student student; // 初始一个student结构体
   //  给结构体赋值
   // 方法1:在函数中创建一个storage的struct引用
   function initStudent1() external{
       Student storage _student = student; // assign a copy of student
       _student.id = 11;
       _student.score = 100;
   }
   // 方法2:直接引用状态变量的struct
   function initStudent2() external{
       student.id = 1;
       student.score = 80;
   }
   // 方法3:构造函数式
   function initStudent3() external {
       student = Student(3, 90);
   }
   // 方法4:key value
   function initStudent4() external {
       student = Student({id: 4, score: 60});
   }
}

2024.09.25

映射Mapping

  • 可以通过键(Key)来查询对应的值(Value),比如:通过一个人的id来查询他的钱包地址。
  • 声明映射的格式为mapping(_KeyType => _ValueType),其中_KeyType_ValueType分别是KeyValue的变量类型。
  • 映射的规则:
    1. 映射的_KeyType只能选择Solidity内置的值类型,比如uintaddress等,不能用自定义的结构体。而_ValueType可以使用自定义的类型。
    // 我们定义一个结构体 Struct
    struct Student{
        uint256 id;
        uint256 score; 
    }
    mapping(Student => uint) public testVar;
    1. 映射的存储位置必须是storage,因此可以用于合约的状态变量,函数中的storage变量和library函数的参数。不能用于public函数的参数或返回结果中,因为mapping记录的是一种关系 (key - value pair)。
    2. 如果映射声明为public,那么Solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value
    3. 给映射新增的键值对的语法为_Var[_Key] = _Value,其中_Var是映射变量名,_Key_Value对应新增的键值对。
     function writeMap (uint _Key, address _Value) public{
         idToAddress[_Key] = _Value;
     }

变量初始值

  • 值类型初始值:
    • 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空白函数
  • 引用类型初始值:
    • 映射mapping: 所有元素都为其默认值的mapping
    • 结构体struct: 所有成员设为其默认值的结构体
    • 数组array
    • 动态数组: []
    • 静态数组(定长): 所有成员设为其默认值的静态数组
    // Reference Types
    uint[8] public _staticArray; // 所有成员设为其默认值的静态数组[0,0,0,0,0,0,0,0]
    uint[] public _dynamicArray; // `[]`
    mapping(uint => address) public _mapping; // 所有元素都为其默认值的mapping
    // 所有成员设为其默认值的结构体 0, 0
    struct Student{
        uint256 id;
        uint256 score; 
    }
    Student public student;

delete操作符

  • delete a会让变量a的值变为初始值。
// delete操作符
bool public _bool2 = true; 
function d() external {
    delete _bool2; // delete 会让_bool2变为默认值,false
}

constant和immutable

  • constant
    • constant变量必须在声明的时候初始化,之后再也不能改变。尝试改变的话,编译不通过。
    // constant变量必须在声明的时候初始化,之后不能改变
    uint256 constant CONSTANT_NUM = 10;
    string constant CONSTANT_STRING = "0xAA";
    bytes constant CONSTANT_BYTES = "WTF";
    address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000;
  • immutable
    • immutable变量可以在声明时或构造函数中初始化。在Solidity v8.0.21以后,不需要显式初始化。反之,则需要显式初始化。
    • 若immutable变量既在声明时初始化,又在constructor中初始化,会使用constructor初始化的值。
    // immutable变量可以在constructor里初始化,之后不能改变
    uint256 public immutable IMMUTABLE_NUM = 9999999999;
    address public immutable IMMUTABLE_ADDRESS;
    uint256 public immutable IMMUTABLE_BLOCK;
    uint256 public immutable IMMUTABLE_TEST;
    • 可以使用全局变量例如address(this)block.number 或者自定义的函数给immutable变量初始化。
    // 利用constructor初始化immutable变量,因此可以利用
    constructor(){
        IMMUTABLE_ADDRESS = address(this);
        IMMUTABLE_NUM = 1118;
        IMMUTABLE_TEST = test();
    }
    
    function test() public pure returns(uint256){
        uint256 what = 9;
        return(what);
    }

2024.09.26

控制流

  • if-elseforwhiledo while、三元运算符
  • 插入排序: uint是正整数,取到负值的话,会报underflow错误,要注意。
// 插入排序 正确版
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);
}

构造函数

  • constructor: 是一种特殊的函数,每个合约可以定义一个,并在部署合约的时候自动运行一次。它可以用来初始化合约的一些参数,例如初始化合约的owner地址
address owner; // 定义owner变量

// 构造函数
constructor(address initialOwner) {
    owner = initialOwner; // 在部署合约的时候,将owner设置为传入的initialOwner地址
}

修饰器

  • 修饰器(modifier): 类似于面向对象编程中的装饰器(decorator),声明函数拥有的特性,并减少代码冗余。modifier的主要使用场景是运行函数前的检查,例如地址,变量,余额等。
// 定义modifier
modifier onlyOwner {
   require(msg.sender == owner); // 检查调用者是否为owner地址
   _; // 如果是的话,继续运行函数主体;否则报错并revert交易
}
function changeOwner(address _newOwner) external onlyOwner{
   owner = _newOwner; // 只有owner地址运行这个函数,并改变owner
}

事件

  • 事件(event): 是EVM上日志的抽象,它具有两个特点:
    1. 响应:应用程序(ethers.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。
    2. 经济:事件是EVM上比较经济的存储数据的方式,每个大概消耗2,000 gas;相比之下,链上存储一个新变量至少需要20,000 gas。
  • 声明事件: 事件的声明由event关键字开头,接着是事件名称,括号里面写好事件需要记录的变量类型和变量名。
  • 释放事件:
event Transfer(address indexed from, address indexed to, uint256 value);
// 定义_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

  • 日志的第一部分是主题数组,用于描述事件,长度不能超过4。它的第一个元素是事件的签名(哈希)。
keccak256("Transfer(address,address,uint256)")

//0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
  • 除了事件哈希,主题还可以包含至多3个indexed参数,也就是Transfer事件中的fromto
  • indexed标记的参数可以理解为检索事件的索引“键”,方便之后搜索。每个indexed参数的大小为固定的256bits,如果参数太大了(比如字符串),就会自动计算哈希存储在主题中。

数据 data

  • 事件中不带indexed的参数会被存储在data部分中,可以理解为事件的“值”。
  • data部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般data部分可以用来存储复杂的数据结构,例如数组和字符串等等。
  • data部分的变量在存储上消耗的gas相比于topics更少。

2024.09.27

继承

  • 规则:
    1. virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。
    2. override:子合约重写了父合约中的函数,需要加上override关键字。
      • 注意:用override修饰public变量,会重写与变量同名的getter函数
  • 简单继承:
contract Yeye {
    event Log(string msg);

    // 定义3个function: hip(), pop(), man(),Log值为Yeye。
    function hip() public virtual{
        emit Log("Yeye");
    }

    function pop() public virtual{
        emit Log("Yeye");
    }

    function yeye() public virtual {
        emit Log("Yeye");
    }
}
contract Baba is Yeye{
    // 继承两个function: hip()和pop(),输出改为Baba。
    function hip() public virtual override{
        emit Log("Baba");
    }

    function pop() public virtual override{
        emit Log("Baba");
    }

    function baba() public virtual{
        emit Log("Baba");
    }
}
  • 多重继承:
    • 规则:
      1. 继承时要按辈分最高到最低的顺序排。
      2. 如果某一个函数在多个继承的合约里都存在,比如例子中的hip()和pop(),在子合约里必须重写,不然会报错。
      3. 重写在多个父合约中都重名的函数时,override关键字后面要加上所有父合约名字,例如override(Yeye, Baba)
    contract Erzi is Yeye, Baba{
        // 继承两个function: hip()和pop(),输出值为Erzi。
        function hip() public virtual override(Yeye, Baba){
            emit Log("Erzi");
        }
    
        function pop() public virtual override(Yeye, Baba) {
            emit Log("Erzi");
        }
    }
  • 修饰器的继承:
contract Base1 {
    modifier exactDividedBy2And3(uint _a) virtual {
        require(_a % 2 == 0 && _a % 3 == 0);
        _;
    }
}

contract Identifier is Base1 {

    //计算一个数分别被2除和被3除的值,但是传入的参数必须是2和3的倍数
    function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) {
        return getExactDividedBy2And3WithoutModifier(_dividend);
    }

    //计算一个数分别被2除和被3除的值
    function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){
        uint div2 = _dividend / 2;
        uint div3 = _dividend / 3;
        return (div2, div3);
    }
}
  • 也可以利用override关键字重写修饰器
modifier exactDividedBy2And3(uint _a) override {
    _;
    require(_a % 2 == 0 && _a % 3 == 0);
}
  • 构造函数的继承:
    1. 在继承时声明父构造函数的参数
    2. 在子合约的构造函数中声明构造函数的参数
    // 构造函数的继承
    abstract contract A {
        uint public a;
    
        constructor(uint _a) {
            a = _a;
        }
    }
    contract C is A {
        constructor(uint _c) A(_c * _c) {}
    }
  • 调用父合约的函数:
    1. 直接调用:子合约可以直接用父合约名.函数名()的方式来调用父合约函数
    function callParentSuper() public{
       // 将调用最近的父合约函数,Baba.pop()
       super.pop();
    }
    1. super关键字:子合约可以利用super.函数名()来调用最近的父合约函数。
      • Solidity继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Babasuper.pop()将调用Baba.pop()而不是Yeye.pop()
    function callParentSuper() public{
        // 将调用最近的父合约函数,Baba.pop()
        super.pop();
    }
  • 钻石继承: 在面向对象编程中,钻石继承(菱形继承)指一个派生类同时有两个或两个以上的基类。
    • 在多重+菱形继承链条上使用super关键字时,需要注意的是使用super会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

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

contract God {
    event Log(string message);

    function foo() public virtual {
        emit Log("God.foo called");
    }

    function bar() public virtual {
        emit Log("God.bar called");
    }
}

contract Adam is God {
    function foo() public virtual override {
        emit Log("Adam.foo called");
        super.foo();
    }

    function bar() public virtual override {
        emit Log("Adam.bar called");
        super.bar();
    }
}

contract Eve is God {
    function foo() public virtual override {
        emit Log("Eve.foo called");
        super.foo();
    }

    function bar() public virtual override {
        emit Log("Eve.bar called");
        super.bar();
    }
}

contract people is Adam, Eve {
    function foo() public override(Adam, Eve) {
        super.foo();
    }

    function bar() public override(Adam, Eve) {
        super.bar();
    }
}

2024.09.29

09.28內容不見了,重打一遍。

抽象合约

  • 如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体{}中的内容,则必须将该合约标为abstract
  • 未实现的函数需要加virtual,以便子合约重写。
abstract contract InsertionSort{
    function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);
}

接口

  • 类似于抽象合约,但它不实现任何功能。
  • 接口的规则:
    1. 不能包含状态变量。
    2. 不能包含构造函数。
    3. 不能继承除接口外的其他合约。
    4. 所有函数都必须是external且不能有函数体。
    5. 继承接口的非抽象合约必须实现接口定义的所有功能。
  • 接口是智能合约的骨架,定义了合约的功能以及如何触发它们。
  • 如果智能合约实现了某种接口(比如ERC20ERC721),其他Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息:
    1. 合约里每个函数的bytes4选择器,以及函数签名函数名(每个参数类型)
    2. 接口id。
  • 接口与合约ABI(Application Binary Interface)等价,可以相互转换。
  • 编译接口可以得到合约的ABI,利用abi-to-sol工具,也可以将ABI json文件转换为接口sol文件。
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;
}
  • 我们可以看到,接口和常规合约的区别在于每个函数都以;代替函数体{ }结尾。

IERC721事件

IERC721包含3个事件,其中TransferApproval事件在ERC20中也有。

  1. Transfer事件:在转账时被释放,记录代币的发出地址from,接收地址totokenId
  2. Approval事件:在授权时被释放,记录授权地址owner,被授权地址approvedtokenId
  3. ApprovalForAll事件:在批量授权时被释放,记录批量授权的发出地址owner,被授权地址operator和授权与否的approved

IERC721函数

  1. balanceOf:返回某地址的NFT持有量balance
  2. ownerOf:返回某tokenId的主人owner
  3. transferFrom:普通转账,参数为转出地址from,接收地址totokenId
  4. safeTransferFrom:安全转账(如果接收方是合约地址,会要求实现ERC721Receiver接口)。参数为转出地址from,接收地址totokenId
  5. approve:授权另一个地址使用你的NFT。参数为被授权地址approvetokenId
  6. getApproved:查询tokenId被批准给了哪个地址。
  7. setApprovalForAll:将自己持有的该系列NFT批量授权给某个地址operator
  8. isApprovedForAll:查询某地址的NFT是否批量授权给了另一个operator地址。
  9. safeTransferFrom:安全转账的重载函数,参数里面包含了data

什么时候使用接口?

  • 一个合约实现了IERC721接口,我们不需要知道它具体代码实现,就可以与它交互。
 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
    • 方便且高效(省gas)地向用户解释操作失败的原因
    • 抛出异常的同时可携带参数
    • 可以在contract之外定义异常
      error TransferNotOwner(); // 自定义error
      error TransferNotOwner(address sender); // 自定义的带参数的error
    
      function transferOwner1(uint256 tokenId, address newOwner) public {
        if(_owners[tokenId] != msg.sender){
            revert TransferNotOwner();
            // revert TransferNotOwner(msg.sender);
        }
        _owners[tokenId] = newOwner;
      }
    • error必须搭配revert(回退)命令使用。
  • Require
    • 唯一的缺点就是gas随着描述异常的字符串长度增加,比error命令要高。
    • 使用方法:require(检查条件,"异常的描述")
    function transferOwner2(uint256 tokenId, address newOwner) public {
        require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
        _owners[tokenId] = newOwner;
    }
  • Assert
    • 一般用于程序员写程序debug
    • 不能解释抛出异常的原因(比require少个字符串)
    • 使用方法:assert(检查条件)
    function transferOwner3(uint256 tokenId, address newOwner) public {
        assert(_owners[tokenId] == msg.sender);
        _owners[tokenId] = newOwner;
    }

2024.09.30

重载

  • 名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。
  • Solidity不允许修饰器(modifier)重载。
  • 函数重载:
function saySomething() public pure returns(string memory){
    return("Nothing");
}

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

image

  • 实参匹配(Argument Matching):
    • 在调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。
    • 如果出现多个匹配的重载函数,则会报错。
    function f(uint8 _in) public pure returns (uint8 out) {
       out = _in;
    }
     
    function f(uint256 _in) public pure returns (uint256 out) {
       out = _in;
    }
    • 调用f(50),因为50既可以被转换为uint8,也可以被转换为uint256,因此会报错。

库合约

  • 是一系列的函数合集
  • 和普通合约的不同:
    1. 不能存在状态变量
    2. 不能够继承或被继承
    3. 不能接收以太币
    4. 不可以被销毁
  • 函数可见性如果被设置为public或者external,则在调用函数时会触发一次delegatecall。如果被设置为internal,则不会引起。
  • 对于设置为private可见性的函数来说,其仅能在库合约中可见,在其他合约中不可用。

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. 利用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();
      }
    2. 通过库合约名称调用函数
      // 直接通过库合约名调用
      function getString2(uint256 _number) public pure returns(string memory){
          return Strings.toHexString(_number);
      }
    image 两种方法均能返回正确的16进制string “0xaa”。证明我们调用库合约成功!

2024.10.01

複習 變量類型

  • 值類型: 布林、整數、地址、字節數組。直接傳遞數據。
  • 引用類型: 字串、數組、結構體。只存指向數據的引用。
    • 數組:
      1. 固定長度array: 以memory修飾的動態數組,可用new創建,但必須宣告長度,且宣告後長度不能改變。
      2. 可變長度array
    • 結構體: 定義新的類型,裡面可以是值類型也可以是引用類型。本身也可作為數組或映射類型使用。
  • 映射類型: mapping。只存在stroage
    • 透過key來查詢對應的value

變量作用域

  • 狀態變量: 存在鏈上,合約內、函數外宣告,合約內函數皆可訪問,gas高。
  • 局部變量: 存在記憶體上,函數內宣告,生命週期為函數執行期間,gas低。
  • 全域變量: 在全域範圍工作,是Solidity預留的關鍵字,可不宣告就使用。
    • eg. 乙太單位、時間單位

函數

  • 需定義可見性: internal、external、public、private
    • internal: 只能從本合約內部訪問,繼承的合約可使用。
    • external: 只能從合約外部訪問(內部可通過this.f()調用)。
    • public: 內外部均可見。
    • private: 只能從本合約內部訪問,繼承的合約不可使用。
  • 決定函數權限或功能: pure、view、payable
    • pure: 不能讀也不能寫入鏈上
    • view: 能讀不能寫入鏈上
    • payable: 可支付的
  • 返回: 命令式返回、解構式返回
    • 命令式返回: returns中標明返回變量的名稱,會自動返回,不需使用return。
    • 解構式返回: 聲明變量,將要賦值的變量用 , 隔開,依序排列。若是只讀取部分返回值,則留空。

2024.10.02

變量初始值

  • 值类型初始值:
    • boolean: false
    • string: ""
    • int: 0
    • uint: 0
    • enum: 枚举中的第一个元素
    • address: 0x0000000000000000000000000000000000000000 (或 address(0))
    • function
    • internal: 空白函数
    • external: 空白函数
  • 引用类型初始值:
    • 映射mapping: 所有元素都为其默认值的mapping
    • 结构体struct: 所有成员设为其默认值的结构体
    • 数组array
    • 动态数组: []
    • 静态数组(定长): 所有成员设为其默认值的静态数组

常數 * constantimmutable * constant: 聲明時初始化,不可改變 * immutable: 聲明或在構造函數初始化,若同時,會採用構造函數的值。

控制流

  • if-elseforwhiledo while、三元運算子

構造函數

  • constructor: 每個合約可定義一個,部屬合約時自動執行一次

修飾器

  • modifier: 聲明函數擁有的特性,主要用來在執行函數前的檢查。

事件

  • event: 是EVM上日記的抽象,有兩個特色:
    1. 響應: 應用程式(ethers.js)可以透過RPC街口訂閱和監聽這些事件,並在前端做響應。
    2. 經濟: 比較經濟的存數據方式,每個大概消耗2000gas,但鏈上存一個變量要消耗20000gas
  • 聲明事件: event event_name( 變量類型 變量名, ... )
  • 釋放事件: emit event event_name( 變量名, ... )

EVM日記 Log

  • 以太坊虛擬機(EVM)用日記Log來儲存Solidity事件,每條日記都包含主題topics和數據data兩部分
    1. topics: 描述事件,長度不超過4,第一個元素是事件的簽名(哈希),除了哈希還可以包含最多三個indexed參數,固定為256bits
    2. data: event中不帶indexed的參數,即為事件的值。不能被直接搜尋,但可以存任意大小的數據,消耗的gas相對於topics`更少。

2024.10.03

繼承

  • 規則:
    1. virtual: 希望子合約重寫的函數,需要在父合約中加上virtual
    2. override: 子合約重寫的函數要加上override。且用override修飾public變量,會重寫與變量同名的getter函數
  • 簡單繼承: contract A is B,A是子合約,B是父合約
  • 多重繼承:
    • 規則:
      1. 繼承時按照輩份最高到最低順序排
      2. 某一個函數在多個繼承的合約裡都有,子合約就必須重寫
      3. 重寫在多個父合約中都一樣名稱的函數時,override後面要加上所有父合約的名字。e.g. override(A, B)
  • 修飾器的繼承: 跟函數繼承一樣,在對應的地方加virtualoverride就好。
  • 構造函數的繼承:
    1. 在繼承時聲明父合約構造函數的參數
    2. 在子合約的構造函數聲明構造函數的參數
  • 調用父合約的函數:
    1. 直接調用: 父合約名稱.函數名()
    2. super: super.函數名()
  • 鑽石繼承: 一個派生類同時有兩個或兩個以上的基類。
    • 使用super的話會調用繼承鏈條勝的每一個合約的相關函數,而非最近的父合約。
  /* 继承树:
  God
 /  \
Adam Eve
 \  /
people
*/

抽象合约

  • 一個合約裡面至少有一個未實現的函數,必須將該合約標為abstract
  • 未實現的函數需加virtual,以便子合約重寫。

介面(接口)

  • 類似抽象合約,但不實現任何功能。
  • 介面的規則:
    1. 不能包含狀態變數。
    2. 不能包含建構子。
    3. 不能繼承除介面外的其他合約。
    4. 所有函數都必須是external且不能有函數體。
    5. 繼承介面的非抽象合約必須實作介面定義的所有功能。
  • 介面是智慧合約的骨架,定義了合約的功能以及如何觸發它們。
  • 如果智慧合約實現了某種介面(例如ERC20ERC721),其他Dapps和智能合約就知道如何與它互動。因為介面提供了兩個重要的資訊:
    1. 合約裡每個函數的bytes4選擇器,以及函數簽名函數名(每個參數類型)
    2. 接口id。
  • 介面與合約ABI(Application Binary Interface)等價,可以互相轉換。
  • 編譯介面可以得到合約的ABI,利用abi-to-sol工具,也可以將ABI json檔轉換為介面sol檔。

IERC721事件

IERC721包含3個事件,其中TransferApproval事件在ERC20中也有。

  1. Transfer事件:在轉帳時被釋放,記錄代幣的發出地址from,接收地址totokenId
  2. Approval事件:在授權時被釋放,記錄授權位址owner,被授權位址approvedtokenId
  3. ApprovalForAll事件:在批量授權時被釋放,記錄批量授權的發出地址owner,被授權地址operator和授權與否的approved

IERC721函數

  1. balanceOf:傳回某地址的NFT持有量balance
  2. ownerOf:回傳某tokenId的主人owner
  3. transferFrom:普通轉賬,參數為轉出地址from,接收地址totokenId
  4. safeTransferFrom:安全轉帳(如果接收方是合約位址,會要求實作ERC721Receiver介面)。參數為轉出位址from,接收位址totokenId
  5. approve:授權另一個位址使用你的NFT。參數為被授權位址approvetokenId
  6. getApproved:查詢tokenId被核准給了哪個位址。
  7. setApprovalForAll:將自己持有的該系列NFT批次授權給某個位址operator
  8. isApprovedForAll:查詢某個位址的NFT是否批次授權給了另一個operator位址。
  9. safeTransferFrom:安全轉帳的重載函數,參數裡麵包含了data

什麼時候使用介面

  • 一個合約實現了IERC721接口,我們不需要知道它具體代碼實現,就可以與它交互。

異常

  • 檢查條件不成立的時候,就會拋出異常。
  • Error
    • 方便且有效率(省gas)地向使用者解釋操作失敗的原因
    • 拋出例外的同時可攜帶參數
    • 可以在​​contract之外定義異常
    • error必須搭配revert(回退)指令使用。
  • Require
    • 唯一的缺點就是gas隨著描述異常的字串長度增加,比error指令要高。
    • 使用方法:require(檢查條件,"異常的描述")
  • Assert
    • 一般用於程式設計師寫入程式debug
    • 不能解釋拋出異常的原因(比require少個字串)
    • 使用方法:assert(檢查條件)

2024.10.04

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';

2024.10.05

接收ETH函数 receive

  • receive()函数是在合约收到ETH转账时被调用的函数
  • 一个合约最多有一个receive()函数
  • 声明不需要function关键字: receive() external payable { ... }
  • receive()函数不能有任何的参数,不返回任何值,必须包含externalpayable
  • 合约接收ETH的时候,receive()会被触发
  • 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);
}

回退函数 fallback

  • fallback()函数会在调用合约不存在的函数时被触发
  • 可用于接收ETH,也可以用于代理合约proxy contract
  • 声明须由external修饰,一般也会用payable修饰,用于接收ETH: fallback() external payable { ... }
event fallbackCalled(address Sender, uint Value, bytes Data);

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

receive和fallback的区别

触发fallback() 还是 receive()?
           接收ETH
              |
         msg.data是空?
            /  \
          是    否
          /      \
receive()存在?   fallback()
        / \
       是  否
      /     \
receive()   fallback()
  • 合约接收ETH时,msg.data为空且存在receive()时,会触发receive();msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable。
  • receive()payable fallback()均不存在的时候,向合约直接发送ETH将会报错

2024.10.07

接收ETH合约

  • 先部署一个接收ETH合约ReceiveETH。
    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合约

  • 先在发送ETH合约SendETH中实现payable的构造函数和receive(),让我们能够在部署时和部署后向合约转账。
    contract SendETH {
        // 构造函数,payable使得部署的时候可以转eth进去
        constructor() payable{}
        // receive方法,接收eth时被触发
        receive() external payable{}
    }
  • transfer
    • 用法是接收方地址.transfer(发送ETH数额)
    • transfer()的gas限制是2300,足够用于转账,但对方合约的fallback()receive()函数不能实现太复杂的逻辑。
    • transfer()如果转账失败,会自动revert(回滚交易)。
    • 注意里面的_to填ReceiveETH合约的地址,amount是ETH转账金额
    // 用transfer()发送ETH
    function transferETH(address payable _to, uint256 amount) external payable{
        _to.transfer(amount);
    }
  • 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();
        }
    }
  • 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();
        }
    }

2024.10.08

调用已部署合约

  • 先写一个简单的合约OtherContract,用于被其他合约调用。
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;
    }
}
  • 调用OtherContract合约
    • 可以利用合约的地址和合约代码(或接口)来创建合约的引用: _Name(_Address)
    • 用合约的引用来调用它的函数: _Name(_Address).f()
  • 4个调用合约的例子
    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的,那么我们可以通过调用它来给合约转账。
      • e.g. _Name(_Address).f{value: _Value}()
      function setXTransferETH(address otherContract, uint256 x) payable external{
         OtherContract(otherContract).setX{value: msg.value}(x);
      }

2024.10.09

Call

  • address类型的低级成员函数
  • 用来与其他合约交互
  • 返回值为(bool, bytes memory)
  • 是Solidity官方推荐的通过触发fallbackreceive函数发送ETH的方法
  • 不推荐用call来调用另一个合约,因为当你调用不安全合约的函数时,你就把主动权交给了它。
  • 当我们不知道对方合约的源代码或ABI,就没法生成合约变量;这时,我们仍可以通过call调用对方合约的函数。
  • 使用规则:
    1. 目标合约地址.call(字节码);
    2. 字节码利用结构化编码函数获得: abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)
    3. 函数签名"函数名(逗号分隔的参数类型)" e.g. abi.encodeWithSignature("f(uint256,address)", _x, _addr)
    4. call在调用合约时可以指定交易发送的ETH数额和gas数额: 目标合约地址.call{value:发送数额, gas:gas数额}(字节码);

目标合约

  • 先写一个简单的目标合约OtherContract并部署
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事件 Solidity // 定义Response事件,输出call返回的结果success和data event Response(bool success, bytes data); * 写一个Call合约来调用目标合约函数。

    2. 调用setX函数

      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函数来调用目标合约的setX(),转入msg.value数额的ETH,并释放Response事件输出successdata image
    3. 调用getX函数

      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));
      }
      • 调用getX()函数返回目标合约_x的值,可以利用abi.decode来解码call的返回值data,并读出数值。
    4. 调用不存在的函数

      function callNonExist(address _addr) external{
          // call 不存在的函数
          (bool success, bytes memory data) = _addr.call(
              abi.encodeWithSignature("foo(uint256)")
          );
      
          emit Response(success, data); //释放事件
      }
      • 如果我们给call输入的函数不存在于目标合约,那么目标合约的fallback函数会被触发。

2024.10.10

Delegatecall

  • call类似,是Solidity中地址类型的低级成员函数。
  • delegate是委托/代表的意思

delegatecall委托了什么? * 用户A通过合约B来call合约C的时候,执行的是合约C的函数,上下文(Context,可以理解为包含变量和状态的环境)也是合约C的 * msg.sender是B的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约C的变量上。 image * 用户A通过合约B来delegatecall合约C的时候,执行的是合约C的函数,但是上下文仍是合约B的 * msg.sender是A的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约B的变量上。 image

  • delegatecall语法: 目标合约地址.delegatecall(二进制编码);
  • 二进制编码利用结构化编码函数abi.encodeWithSignature获得: abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)
  • call不一样,delegatecall在调用合约时可以指定交易发送的gas,但不能指定发送的ETH数额
  • 使用时要保证当前合约和目标合约的状态变量存储结构相同,并且目标合约安全,不然会造成资产损失。

什么情况下会用到delegatecall?

  • 主要有两个应用场景:
    1. 代理合约(Proxy Contract):
      • 将智能合约的存储合约和逻辑合约分开
      • 代理合约存储所有相关的变量,并且保存逻辑合约的地址
      • 所有函数存在逻辑合约(Logic Contract)里,通过delegatecall执行。当升级时,只需要将代理合约指向新的逻辑合约即可。
    2. EIP-2535 Diamonds(钻石):
      • 支持构建可在生产中扩展的模块化智能合约系统的标准
      • 具有多个实施合约的代理合约

delegatecall例子

  • 调用结构:你(A)通过合约B调用目标合约C。
    • 被调用的合约C:
      // 被调用的合约C
      contract C {
          uint public num;
          address public sender;
      
          function setVars(uint _num) public payable {
              num = _num;
              sender = msg.sender;
          }
      }
    • 发起调用的合约B: 合约B必须和目标合约C的变量存储布局必须相同,两个变量,并且顺序为num和sender
      contract B {
          uint public num;
          address public sender;
      }
    • 分别用calldelegatecall来调用合约C的setVars函数
      // 通过call来调用C的setVars()函数,将改变合约C里的状态变量
      function callSetVars(address _addr, uint _num) external payable{
          // call setVars()
          (bool success, bytes memory data) = _addr.call(
              abi.encodeWithSignature("setVars(uint256)", _num)
          );
      }
    • delegatecallSetVars函数通过delegatecall来调用setVars
      // 通过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)
          );
      }

2024.10.11

去中心化交易所uniswap

  • create: Contract x = new Contract{value: _value}(params)
  • 如果构造函数是payable,可以创建时转入_value数量的ETHparams是新合约构造函数的参数。

极简Uniswap

  • Uniswap V2核心合约中包含两个合约
    1. UniswapV2Pair: 币对合约,用于管理币对地址、流动性、买卖。
    2. UniswapV2Factory: 工厂合约,用于创建新的币对,并管理币对地址。
  • 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;
    }
}
  • 构造函数constructor在部署时将factory赋值为工厂合约地址。initialize函数会由工厂合约在部署完成后手动调用以初始化代币地址,将token0token1更新为币对中两种代币的地址。

    • 为什么uniswap不在constructor中将token0token1地址更新好?
    • 因为uniswap使用的是create2创建合约,生成的合约地址可以实现预测
  • 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;
    }
}
  • getPair是两个代币地址到币对地址的map,方便根据代币找到币对地址;allPairs是币对地址的数组,存储了所有代币地址。
  • PairFactory合约只有一个createPair函数,根据输入的两个代币地址tokenAtokenB来创建新的Pair合约。

2024.10.12

CREATE2

  • 智能合约部署在以太坊网络之前就能预测合约的地址。
  • Uniswap创建Pair合约用的就是CREATE2
  • CREATE如何计算地址
    • 智能合约可以由其他合约和普通账户利用CREATE操作码创建。
    • 新合约的地址计算:创建者的地址(通常为部署的钱包地址或者合约地址)和nonce(该地址发送交易的总数,对于合约账户是创建的合约总数,每创建一个合约nonce+1)的哈希。
    新地址 = hash(创建者地址, nonce)
    • 创建者地址不会变,但nonce可能会随时间而改变,因此用CREATE创建的合约地址不好预测。
  • CREATE2如何计算地址
    • 为了让合约地址独立于未来的事件。不管未来区块链上发生了什么,你都可以把合约部署在事先计算好的地址上。
    • 用CREATE2创建的合约地址由4个部分决定:
      1. 0xFF:一个常数,避免和CREATE冲突
      2. CreatorAddress: 调用CREATE2的当前合约(创建合约)地址。
      3. salt(盐):一个创建者指定的bytes32类型的值,它的主要目的是用来影响新创建的合约的地址。
      4. initcode: 新合约的初始字节码(合约的Creation Code和构造函数的参数)
      新地址 = hash("0xFF",创建者地址, salt, initcode)
    • 如果创建者使用CREATE2和提供的salt部署给定的合约initcode,它将存储在新地址中。

如何使用CREATE2

  • Contract x = new Contract{salt: _salt, value: _value}(params)
  • Contract是要创建的合约名,x是合约对象(地址),_salt是指定的盐;如果构造函数是payable,可以创建时转入_value数量的ETHparams是新合约构造函数的参数。

用CREATE2来实现极简Uniswap

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;
    }
}

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

    function createPair2(address tokenA, address tokenB) external returns (address pairAddr) {
        require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突
        // 用tokenA和tokenB地址计算salt
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        // 用create2部署新合约
        Pair pair = new Pair{salt: salt}(); 
        // 调用新合约的initialize方法
        pair.initialize(tokenA, tokenB);
        // 更新地址map
        pairAddr = address(pair);
        allPairs.push(pairAddr);
        getPair[tokenA][tokenB] = pairAddr;
        getPair[tokenB][tokenA] = pairAddr;
    }
}
  • 事先计算Pair地址
// 提前计算pair合约地址
function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress){
    require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突
    // 计算用tokenA和tokenB地址计算salt
    (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    // 计算合约地址方法 hash()
    predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
        bytes1(0xff),
        address(this),
        salt,
        keccak256(type(Pair).creationCode)
        )))));
}
  • 如果部署合约构造函数中存在参数 e.g. Pair pair = new Pair{salt: salt}(address(this));
  • 计算时,需要将参数和initcode一起进行打包 e.g. keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this))))
predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
                bytes1(0xff),
                address(this),
                salt,
                keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this))))
            )))));

2024.10.13

selfdestruct

  • 用来删除智能合约,并将该合约剩余ETH转到指定地址。
  • 当前SELFDESTRUCT仅会被用来将合约中的ETH转移到指定地址,而原先的删除功能只有在合约创建-自毁这两个操作处在同一笔交易时才能生效。
  • 所以:
    1. 已经部署的合约无法被SELFDESTRUCT了。
    2. 如果要使用原先的SELFDESTRUCT功能,必须在同一笔交易中创建并SELFDESTRUCT

如何使用selfdestruct

  • selfdestruct(_addr);
  • _addr是接收合约中剩余ETH的地址。_addr地址不需要有receive()fallback()也能接收ETH

转移ETH功能

  • 在坎昆升级前可以完成合约的自毁,在坎昆升级后仅能实现内部ETH余额的转移。
contract DeleteContract {
    uint public value = 10;   
    constructor() payable {}  
    receive() external payable {}

    function deleteContract() external {
        // 调用selfdestruct销毁合约,并把剩余的ETH转给msg.sender
        selfdestruct(payable(msg.sender));
    }

    function getBalance() external view returns(uint balance){
        balance = address(this).balance;
    }
}
  • 部署好合约后,我们向DeleteContract合约转入1ETH。这时,getBalance()会返回1ETHvalue变量是10。
  • 调用deleteContract()函数,合约将触发selfdestruct操作。在坎昆升级前,合约会被自毁。但是在升级后,合约依然存在,只是将合约包含的ETH转移到指定地址,而合约依然能够调用。

同笔交易内实现合约创建-自毁

  • 原先的删除功能只有在合约创建-自毁这两个操作处在同一笔交易时才能生效。所以我们需要通过另一个合约进行控制。
contract DeployContract {
    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){
        DeleteContract del = new DeleteContract{value:msg.value}();
        DemoResult memory res = DemoResult({
            addr: address(del),
            balance: del.getBalance(),
            value: del.value()
        });
        del.deleteContract();
        return res;
    }
}

注意事项

  1. 对外提供合约销毁接口时,最好设置为只有合约所有者可以调用,可以使用函数修饰符onlyOwner进行函数声明。
  2. 当合约中有selfdestruct功能时常常会带来安全问题和信任问题,合约中的selfdestruct功能会为攻击者打开攻击向量(例如使用selfdestruct向一个合约频繁转入token进行攻击,这将大大节省了GAS的费用,虽然很少人这么做),此外,此功能还会降低用户对合约的信心。

2024.10.14

ABI编码

  • 4个函数

    1. abi.encode: 将给定参数利用ABI规则编码。ABI被设计出来跟智能合约交互,他将每个参数填充为32字节的数据,并拼接在一起。
    uint x = 10;
    address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
    string name = "0xAA";
    uint[2] array = [5, 6];
    
    function encode() public view returns(bytes memory result) {
        result = abi.encode(x, addr, name, array);
    }
    * 编码的结果为`0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`,由于abi.encode将每个数据都填充为32字节,中间有很多0。
    
    1. abi.encodePacked: 将给定参数根据其所需最低空间编码。当你想省空间,并且不与合约交互的时候,可以使用。
    function encodePacked() public view returns(bytes memory result) {
    result = abi.encodePacked(x, addr, name, array);
    }
    * 编码的结果为`0x000000000000000000000000000000000000000000000000000000000000000a7a58c0be72be218b41c608b7fe7c5bb630736c713078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006`,由于abi.encodePacked对编码进行了压缩,长度比abi.encode短很多。
    
    1. abi.encodeWithSignature: 与abi.encode功能类似,只不过第一个参数为函数签名,比如foo(uint256,address,string,uint256[2])。当调用其他合约的时候可以使用。
    function encodeWithSignature() public view returns(bytes memory result) {
       result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
    }
    • 编码的结果为0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000,等同于在abi.encode编码结果前加上了4字节的函数选择器(通过函数名和参数进行签名处理(Keccak–Sha3)来标识函数,可以用于不同合约之间的函数调用)。
    1. abi.encodeWithSelector: 与abi.encodeWithSignature功能类似,只不过第一个参数为函数选择器,为函数签名Keccak哈希的前4个字节。
    function encodeWithSelector() public view returns(bytes memory result) {
       result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
    }
    • 编码的结果为 0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000,与abi.encodeWithSignature结果一样。

ABI解码

  • abi.decode: abi.decode用于解码abi.encode生成的二进制编码,将它还原成原本的参数。
    function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) {
       (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2]));
    }
    image

Hash的性质

  • 可以将任意长度的消息转换为一个固定长度的值,这个值也称作哈希(hash)。
  • 一个好的哈希函数应该具有以下几个特性:
    • 单向性:从输入的消息到它的哈希的正向运算简单且唯一确定,而反过来非常难,只能靠暴力枚举。
    • 灵敏性:输入的消息改变一点对它的哈希改变很大。
    • 高效性:从输入的消息到哈希的运算高效。
    • 均一性:每个哈希值被取到的概率应该基本相等。
    • 抗碰撞性:
      • 弱抗碰撞性:给定一个消息x,找到另一个消息x',使得hash(x) = hash(x')是困难的。
      • 强抗碰撞性:找到任意x和x',使得hash(x) = hash(x')是困难的。
  • 应用
    • 生成数据唯一标识
    • 加密签名
    • 安全加密

Keccak256

  • Keccak256函数是Solidity中最常用的哈希函数,用法非常简单: 哈希 = keccak256(数据);
  • Keccak256和sha3:
    1. sha3由keccak标准化而来,在很多场合下Keccak和SHA3是同义词,但在2015年8月SHA3最终完成标准化时,NIST调整了填充算法。所以SHA3就和keccak计算的结果不一样,这点在实际开发中要注意。
    2. 以太坊在开发的时候sha3还在标准化中,所以采用了keccak,所以Ethereum和Solidity智能合约代码中的SHA3是指Keccak256,而不是标准的NIST-SHA3,为了避免混淆,直接在合约代码中写成Keccak256是最清晰的。
  • 生成数据唯一标识: 我们有几个不同类型的数据:uintstringaddress,我们可以先用abi.encodePacked方法将他们打包编码,然后再用keccak256来生成唯一标识:
    function hash(
        uint _num,
        string memory _string,
        address _addr
        ) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_num, _string, _addr));
    }
  • 弱抗碰撞性: 给定一个消息0xAA,试图去找另一个消息,使得它们的哈希值相等:
    function weak(
        string memory string1
        )public view returns (bool){
        return keccak256(abi.encodePacked(string1)) == _msg;
    }
  • 强抗碰撞性: 构造一个函数strong,接收两个不同的string参数string1string2,然后判断它们的哈希是否相同
    function strong(
       string memory string1,
       string memory string2
       )public pure returns (bool){
       return keccak256(abi.encodePacked(string1)) == keccak256(abi.encodePacked(string2));
    }

2024.10.15

函数选择器

  • 当我们调用智能合约时,本质上是向目标合约发送了一段calldata,发送的calldata中前4个字节是selector(函数选择器)
  • msg.data: 是Solidity中的一个全局变量,值为完整的calldata(调用函数时传入的数据)。
    // event 返回msg.data
    event Log(bytes data);
    
    function mint(address to) external{
        emit Log(msg.data);
    }
    • 当参数为0x2c44b726ADF1963cA47Af88B284C06f30380fC78时,输出的calldata0x6a6278420000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78
    • 前4个字节为函数选择器selector;后面32个字节为输入的参数。
    • calldata就是告诉智能合约,我要调用哪个函数,以及参数是什么。

method id、selector和函数签名

  • method id定义为函数签名的Keccak哈希后的前4个字节,当selectormethod id相匹配时,即表示调用该函数>。
  • 计算method id时,需要通过函数名和函数的参数类型来计算,Solidity中,函数的参数类型主要分为:基础类型参数,固定长度类型参数,可变长度类型参数和映射类型参数。
    1. 基础类型参数: e.g. bytes4(keccak256("函数名(参数类型1,参数类型2,...)"))
    2. 固定长度类型参数: e.g. bytes4(keccak256("fixedSizeParamSelector(uint256[3])"))
    3. 可变长度类型参数: e.g. bytes4(keccak256("nonFixedSizeParamSelector(uint256[],string)"))
    4. 映射类型参数: e.g. bytes4(keccak256("mappingParamSelector(address,(uint256,bytes),uint256[],uint8)"))

使用selector

  • 我们可以利用selector来调用目标函数。
  • 例如我想调用elementaryParamSelector函数,我只需要利用abi.encodeWithSelector将elementaryParamSelector函数的method id作为selector和参数打包编码,传给call函数
    // 使用selector来调用函数
    function callWithSignature() external{
    ...
        // 调用elementaryParamSelector函数
        (bool success1, bytes memory data1) = address(this).call(abi.encodeWithSelector(0x3ec37834, 1, 0));
    ...
    }

2024.10.16

try-catch

  • 只能被用于external函数或创建合约时constructor(被视为external函数)的调用。
try externalContract.f() { // 某个外部合约的函数调用
    // call成功的情况下 运行一些代码
} catch {
    // call失败的情况下 运行一些代码
}
  * 可以使用`this.f()`来替代`externalContract.f()`
  * `this.f()`也被视作为外部调用,但不可在构造函数中使用,因为此时合约还未创建。
  * 如果调用的函数有返回值,那么必须在`try`之后声明`returns(returnType val)`,并且在`try`模块中可以使用返回的变量
  * 如果是创建合约,那么返回值是新创建的合约变量。
try externalContract.f() returns(returnType val){
    // call成功的情况下 运行一些代码
} catch {
    // call失败的情况下 运行一些代码
}
  • catch模块支持捕获特殊的异常原因
try externalContract.f() returns(returnType){
    // call成功的情况下 运行一些代码
} catch Error(string memory /*reason*/) {
    // 捕获revert("reasonString") 和 require(false, "reasonString")
} catch Panic(uint /*errorCode*/) {
    // 捕获Panic导致的错误 例如assert失败 溢出 除零 数组访问越界
} catch (bytes memory /*lowLevelData*/) {
    // 如果发生了revert且上面2个异常类型匹配都失败了 会进入该分支
    // 例如revert() require(false) revert自定义类型的error
}

try-catch实战

  • 创建一个外部合约OnlyEven,并使用try-catch来处理异常
contract OnlyEven{
    constructor(uint a){
        require(a != 0, "invalid number");
        assert(a != 1);
    }

    function onlyEven(uint256 b) external pure returns(bool success){
        // 输入奇数时revert
        require(b % 2 == 0, "Ups! Reverting");
        success = true;
    }
}
  • 处理外部函数调用异常
// 成功event
event SuccessEvent();

// 失败event
event CatchEvent(string message);
event CatchByte(bytes data);

// 声明OnlyEven合约变量
OnlyEven even;

constructor() {
    even = new OnlyEven(2);
}

// 在external call中使用try-catch
function execute(uint amount) external returns (bool success) {
    try even.onlyEven(amount) returns(bool _success){
        // call成功的情况下
        emit SuccessEvent();
        return _success;
    } catch Error(string memory reason){
        // call不成功的情况下
        emit CatchEvent(reason);
    }
}
  • 处理合约创建异常: 只需要把try模块改写为OnlyEven合约的创建就行
// 在创建新合约中使用try-catch (合约创建被视为external call)
// executeNew(0)会失败并释放`CatchEvent`
// executeNew(1)会失败并释放`CatchByte`
// executeNew(2)会成功并释放`SuccessEvent`
function executeNew(uint a) external returns (bool success) {
    try new OnlyEven(a) returns(OnlyEven _even){
        // call成功的情况下
        emit SuccessEvent();
        success = _even.onlyEven(a);
    } catch Error(string memory reason) {
        // catch失败的 revert() 和 require()
        emit CatchEvent(reason);
    } catch (bytes memory reason) {
        // catch失败的 assert()
        emit CatchByte(reason);
    }
}