Solidity Development Book

Welcome to the world of decentralized blockchain: The document begins with an introduction to blockchain technology and Ethereum, providing essential context for understanding smart contracts. It then delves into the syntax and features of Solidity, covering key concepts such as data types, functions, modifiers, and inheritance. Practical examples are included to illustrate how to write and deploy smart contracts, along with best practices for security and optimization. Additionally, the document addresses common pitfalls and debugging strategies to help learners navigate challenges they may encounter. Finally, the document provides resources for further learning, including links to online courses, documentation, and community forums. This structured approach aims to equip readers with the knowledge and skills needed to confidently create and manage their own smart contracts in Solidity

This book will guide you through the development of a decentralized application, including:

  • smart-contract development (in Solidity);

This book is not for complete beginners.

I expect you to be an experienced developer, who has ever programmed in any programming language. It’ll also be helpful if you know the syntax of Solidity, the main programming language of this book. If not, it’s not a big problem: we’ll learn a lot about Solidity and Ethereum Virtual Machine during our journey.

However, this book is for blockchain beginners.

If you only heard about blockchains and were interested but haven’t had a chance to dive deeper, this book is for you! Yes, for you! You’ll learn how to develop for blockchains (specifically, Ethereum), how blockchains work, how to program and deploy smart contracts, and how to run and test them on your computer.

Alright, let’s get started!

  1. This book is hosted on GitHub: https://github.com/yuhuajing/solidity-book

Data Types

数据类型

  • Solidity EVM 在宽 256bit2^256 的栈空间存储合约数据
  • 合约内部数据分为定长数值类型和非定长的引用类型

数值类型

数值类型赋值时直接传递值,包含 boolean,整数型(uint8~uint256,int8~int256),address,定长bytes(bytes1~bytes32)

  • boolean 类型是二值变量, 取值 true|false,default:false
    • 运算符包括:
    • !(非)
    • && (与,短路规则,如果前者false,就不会执行后者)
    • || (或,短路规则,如果前者true,就不会执行后者)
    • == (判等)
    • != (不等)
  • uint/int 整型,default: 0
    • 运算符包括:
    • 比较运算符,返回bool (> < >= <= == !=)
    • 算数运算符(+ - * / % ** << >>)
  • address类型
    • address 类型,可以使用 payable() 修饰,用于接收 NativeToken(触发 receiver() 函数或缺省函数 fallback() ) 数值类型合约示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Static_variables {
    bool public boo = true;

    /*
    uint stands for unsigned integer, meaning non negative integers
    different sizes are available
        uint8   ranges from 0 to 2 ** 8 - 1
        uint16  ranges from 0 to 2 ** 16 - 1
        ...
        uint256 ranges from 0 to 2 ** 256 - 1
    */
    uint8 public u8 = 1;
    uint256 public u256 = 456;
    uint256 public u = 123; // uint is an alias for uint256

    /*
    Negative numbers are allowed for int types.
    Like uint, different ranges are available from int8 to int256
    
    int256 ranges from -2 ** 255 to 2 ** 255 - 1
    int128 ranges from -2 ** 127 to 2 ** 127 - 1
    */
    int8 public i8 = -1;
    int256 public i256 = 456;
    int256 public i = -123; // int is same as int256

    // minimum and maximum of uint
    uint256 public minUInt = type(uint256).min;
    uint256 public maxUInt = type(uint256).max;

    // minimum and maximum of int
    int256 public minInt = type(int256).min;
    int256 public maxInt = type(int256).max;

    address public addr = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c;

    /*
    In Solidity, the data type byte represent a sequence of bytes. 
    Solidity presents two type of bytes types :

     - fixed-sized byte arrays
     - dynamically-sized byte arrays.
     
     The term bytes in Solidity represents a dynamic array of bytes. 
     It’s a shorthand for byte[] .
    */
    bytes1 a = 0xb5; //  [10110101]
    bytes1 b = 0x56; //  [01010110]

    // Default values
    // Unassigned variables have a default value
    bool public defaultBoo; // false
    uint256 public defaultUint; // 0
    int256 public defaultInt; // 0
    address public defaultAddr; // 0x0000000000000000000000000000000000000000
    bytes1 public c; //0x00
}

引用类型

引用类型:array[]数组,bytes,定长数组,struct结构体,mapping映射

  • 数组:动态数组拥有 push/pop 内置函数,分别在数组最后增加或删除一个元素
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Dynamic_variables_array {
    // Several ways to initialize an array
    uint256[] public indeterminate_arr;
    uint256[] public indeterminate_init_arr = [1, 2, 3];
    // Fixed sized array, all elements initialize to 0
    uint256[10] public determinate_arr;

    function get(uint256 i) public view returns (uint256) {
        return indeterminate_arr[i];
    }

    // Solidity can return the entire array.
    // But this function should be avoided for
    // arrays that can grow indefinitely in length.
    function getArr() public view returns (uint256[] memory) {
        return indeterminate_arr;
    }

    function indeterminate_push(uint256 i) public {
        // Append to array
        // This will increase the array length by 1.
        indeterminate_arr.push(i);
        indeterminate_init_arr.push(i);
    }

    function determinate_push(uint256 index, uint256 i) public {
        // Append to array
        // This will increase the array length by 1.
        determinate_arr[index] = i;
    }

    function indeterminate_pop() public {
        // Remove last element from array
        // This will decrease the array length by 1
        indeterminate_arr.pop();
        indeterminate_init_arr.pop();
    }

    function getLength() public view returns (uint256) {
        return indeterminate_arr.length;
    }

    function remove_not_change_length(uint256 index) public {
        // Delete does not change the array length.
        // It resets the value at index to it's default value,
        // in this case 0
        delete indeterminate_arr[index];
        delete determinate_arr[index];
    }

    // Deleting an element creates a gap in the array.
    // One trick to keep the array compact is to
    // move the last element into the place to delete.
    function remove_change_length(uint256 index) public {
        // Move the last element into the place to delete
        indeterminate_arr[index] = indeterminate_arr[
            indeterminate_arr.length - 1
        ];
        // Remove the last element
        indeterminate_arr.pop();
    }

    function examples_new_determinate_arr() external pure {
        // create array in memory, only fixed size can be created
        uint256[] memory a = new uint256[](5);
        a[0] = 5;
    }
}

Data Types

变量作用域

  • 状态变量
    • 定义在合约中,但是在函数外的需要存储在合约 slot 的变量
  • 局部变量
    • 定义在合约函数内部,仅在函数执行过程中有效的数据,变量生命周期和函数执行周期一致
  • 全局变量
    • 链上数据,全局变量编码到 EVM 字节码中
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Variables {
    // State variables are stored on the blockchain.
    string public hello = "Hello";
    uint256 public num = 123;

    function interVariables() public view returns (uint256 ts) {
        // Local variables are not saved to the blockchain.
        uint256 i = 456;
        // Here are some global variables
        ts = block.timestamp; // 147 gas cost
        // assembly {
        //     ts := timestamp() // 213 gas cost
        // }
        {
            // 可以使用合约状态变量/本函数内部的局部变量/区块链上全局变量
            uint interTs = 356;
            interTs += block.timestamp;
            interTs += i;
            interTs += num;
        }
       // interTs +=9; 外部无法访问作用域内部的参数
    }
}

Data Types

变量修饰符

参数修饰符包括:public,private,immutable,constant

  • public,自动生成 Getter 函数,表明函数在合约中可以通过 abi 查询
  • private,参数无法通过 abi 直接查询,只能通过自定义的合约函数或 sload(xx) 通过 slot 获得数据
  • immutable,参数必须在构造函数中初始化,并且编码在字节码中,后续无法修改
  • constant,参数在定义时,直接初始化,并且编码在字节码中,后续无法修改
  • payable,用于修饰地址,表明允许该地址接收 NativeToken

变量存储方式

参数在合约中的存储方式

  • 合约数据存储
    • `storage(可修改),Bytecodes(constant/immutable,不可修改)
  • 合约函数运行时的数据存储
    • (memory,stack,calldata)`,传参或返回数据

合约数据存储

  • Bytecodes
    • immutable|constant 变量在合约编译时将值存储在合约代码中,因此后续数据变量无法更改
    • 在合约使用期间,无需在内部存储中维护该常量的状态
    • 由于参数不存储在内部 slot ,数据都是通过字节码读取,因此在合约外部调用中,immutable|constant 的值只会从被调用的合约字节码中获取
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

contract Constants {
  // coding convention to uppercase constant variables
  // 存储在字节码
  address public constant MY_ADDRESS =
  0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc;
  // 存储在字节码
  uint256 public constant MY_UINT = 123;
  // 存储在字节码
  uint256 private immutable a;
  // slot 0
  uint256 public ty = 9;

  constructor() {
    a = 99;
  }

  function getslot(uint256 slot) public view returns (bytes32  value) {
    assembly {
      value := sload(slot)
    }
  }
}
  • Storage
    • 存储合约状态变量,通过写交易修改

运行时数据存储

  • memory. 函数执行过程中用于存储动态分配的数据,如临时变量、函数参数和函数返回值等
  • stack. 函数执行过程中存储数据,如基本数据类型和值类型的局部变量
  • calldata. 和 memory 类似,数据存储在内存中,但是 calldata 数据只读,一般用于函数的输入参数
    function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
        //参数为calldata数组,不能被修改
        // _x[0] = 0 //这样修改会报错
        return(_x);
    }

变量引用作用域

  1. 普通状态变量 -> 普通状态变量(拷贝)
  2. 普通状态变量 -> storage变量(引用)
  3. 普通状态变量 -> memory变量(拷贝)
  4. storage变量 -> storage变量(引用)
  5. storage变量 -> memory变量(拷贝)
  6. storage变量 -> 普通状态变量(引用)
  7. memory变量 -> memory变量(引用)
  8. memory变量 -> 普通状态变量(拷贝)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract StateToStateContract {
  uint8[3] public static_array = [1, 2, 3]; //State 状态变量
  uint8[3] public static_array_two;
  uint256[] public dynamic_array;
  event LogUint8(uint8);
  event staticArrays(uint8[3], uint8[3]);
  event dynamicArrays(uint256[], uint256[]);

  function stateToany() public {
    //状态变量 -> 状态变量(拷贝),双方互不影响
    static_array_two = static_array;
    static_array_two[0] = 8;
    emit staticArrays(static_array, static_array_two); //[1,2,3],[8,2,3]
    //状态变量 -> storage变量(引用),引用拷贝,修改任意变量的值会影响另一个状态变量的值,更新合约状态参数
    uint256[] storage tem_dynamic_array = dynamic_array;
    tem_dynamic_array.push(10086);
    emit dynamicArrays(dynamic_array, tem_dynamic_array); //[10086],[10086]
    //状态变量 -> memory变量(拷贝)
    uint256[] memory tem_dynamic_array_two = dynamic_array;
    tem_dynamic_array_two[tem_dynamic_array_two.length - 1] = 999;
    emit dynamicArrays(dynamic_array, tem_dynamic_array_two); //[10086],[999]
  }

  function storageToany() public {
    //storage变量 -> storage变量(引用),引用拷贝,修改任意变量的值会影响另一个状态变量的值,更新合约状态参数
    uint256[] storage tem_dynamic_array = dynamic_array;
    tem_dynamic_array.push(12);
    emit dynamicArrays(dynamic_array, tem_dynamic_array); //[12],[12]
    uint256[] storage tem_dynamic_array_two = tem_dynamic_array;
    tem_dynamic_array_two.push(13);
    emit dynamicArrays(dynamic_array, tem_dynamic_array); //[12,13],[12,13]
    emit dynamicArrays(tem_dynamic_array, tem_dynamic_array_two); //[12,13],[12,13]
    //storage变量 -> memory变量(拷贝)
    uint256[] memory tem_dynamic_array_memory = tem_dynamic_array;
    tem_dynamic_array_memory[0] = 14;
    emit dynamicArrays(tem_dynamic_array, tem_dynamic_array_memory); //[12,13],[14,13]
    // storage变量 -> 状态变量(引用),引用拷贝,修改任意变量的值会影响另一个状态变量的值,更新合约状态参数
    dynamic_array = tem_dynamic_array;
    dynamic_array[0] = 15;
    emit dynamicArrays(dynamic_array, tem_dynamic_array); //[15,13],[15,13]
    tem_dynamic_array.push(16);
    emit dynamicArrays(dynamic_array, tem_dynamic_array); //[15,13,16],[15,13,16]
  }

  function memoryToany() public {
    // memory变量 -> 状态变量(拷贝)
    uint8[3] memory tem_static_array = static_array;
    tem_static_array[0] = 4;
    emit staticArrays(static_array, tem_static_array); //[1,2,3],[4,2,3]
    // memory变量 -> memory变量(引用),引用拷贝,修改任意变量的值会影响另一个状态变量的值,更新合约状态参数
    uint8[3] memory tem_static_array_two = tem_static_array;
    tem_static_array_two[2] = 5;
    emit staticArrays(static_array, tem_static_array); //[1,2,3],[4,2,5]
    emit staticArrays(tem_static_array, tem_static_array_two); //[4,2,5],[4,2,5]
  }
}

ENUM

  • 枚举 enum 作为用户自定义的变量集合,用于定义有限的多种状态
    • 由于 enum 有限,后续除非重新部署合约,否则无法新增状态
    • enum 适用于确定的有限状态,否则使用 动态数组 更适合后续扩展
  • enum内部的变量从 0 index 开始, default: 0
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

contract enumExamples {
    enum ActionChoices {
        GoLeft,
        GoRight,
        GoStraight,
        SitStill
    }
    ActionChoices choice;
    ActionChoices constant defaultChoice = ActionChoices.GoStraight;

    function setGoStraight() public {
        choice = ActionChoices.GoStraight;
    }

    function setChoice(ActionChoices _choice) public {
        choice = _choice;
    }

    // Since enum types are not part of the ABI, the signature of "getChoice"
    // will automatically be changed to "getChoice() returns (uint8)"
    // for all matters external to Solidity.
    function getChoice() public view returns (ActionChoices) {
        return choice;
    }

    function getDefaultChoice() public pure returns (uint256) {
        return uint256(defaultChoice);
    }

    function getLargestValue() public pure returns (ActionChoices) {
        return type(ActionChoices).max;
    }

    function getSmallestValue() public pure returns (ActionChoices) {
        return type(ActionChoices).min;
    }
}

bytes/string

bytes

  • bytes 是不用加 [] 声明的动态数组
  • hex 修饰 bytes 数据
  • bytes.concat() 直接拼接 bytes 数据
  • 字节数组 bytes 分为定长数值类型和不定长引用类型
    • 定长数值数组(bytes1~bytes32)能通过 index 获取数据
    • 不定长数组 bytes ... 是动态类型,传参时需要 memory|calldata 修饰,作为参数返回时需要 memory 修饰

string

  • stringUTF-8 编码的 bytes 类型
    • stringbytes 数据可以互相转换 string(), bytes()
    • string 传参 ASCII 字符,此时每个字符占据1位,可以通过 index 直接读取值
    • string 传参 Unicode 字符,此时每个字符占据多位,不能通过 index 直接读取值
  • solidity 0.8.12+string.concat() 直接拼接多个 string 字符串
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

contract BytesStringData {
  string private constant NATIE = unicode"👋酱香拿铁";
  string private constant HELL = unicode"Hello 😃";
  string private constant UTF8CODE = "Hello World";
  bytes private hexData = hex"68656C6C6F776F726C64";

  function characterOfString(string memory input, uint256 index)
  external
  pure
  returns (string memory)
  {
    bytes memory char = new bytes(1);
    char[0] = bytes(input)[index];
    return string(char);
  }

  function characterOfStringLength(string memory input)
  external
  pure
  returns (uint256)
  {
    return bytes(input).length;
  }

  function concatMulString() external view returns (string memory) {
    return string.concat(string(hexData), NATIE, UTF8CODE);
  }

  function concatMulBytes() external view returns (bytes memory) {
    return bytes.concat(hexData, bytes(NATIE));
  }
}

Preference

https://www.rareskills.io/learn-solidity/strings

mapping

  • Mapping 只能定义成状态变量,不能在函数内部定义成局部变量
  • Mapping 无法遍历,只能通过 key 值获取对应的 value
  • Mapping 类型不能作为函数的返回值
  • mapping(key ==> value) [public|private] xxx
    • 键值 key 必须是默认的类型,不能使用自定义的类型
    • value 可以是任意类型,包含自定义的结构体数据结构
    • key 值对应的 value 不存在的时候,会返回 value 的默认值,不会 revert
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

contract Mapping {
  // Mapping from address to uint
  mapping(address => uint256) public myMap;
  // Mapping from nft address and user address to his nft id
  mapping(address => mapping(address => uint256)) public addressNFTIds;

  struct myStruct {
    address NFT;
    address Operator;
  }
  // Mapping from nft address and user address to his approvor

  mapping(address => myStruct) public addressApprovedInfo;

  function updateMapping(
    address nft,
    address operator,
    uint256 id
  ) public {
    myMap[msg.sender] = id;
    addressNFTIds[nft][msg.sender] = id;
    addressApprovedInfo[msg.sender] = myStruct(nft, operator);
  }

  function updateOperatorNFT(
    address nft,
    address operator,
    uint256 id
  ) public {
    mapping(address => uint256) storage _tokensByNft = addressNFTIds[nft];
    _tokensByNft[operator] = id;
  }
}

合约逻辑

控制流

  • if(statement){}else{}
  • for(statement){}
    • for 循环长度不定的元素时,一定要控制范围,防止 out-of-gas
  • while(statement){}
    • while 循环执行直到不满足条件,同样在 statement 中要控制循环范围
  • do{}while(statement)
    • 先执行逻辑,在执行判断
    • 执行直到不满足条件,同样在 statement 中要控制循环范围
  • 循环过程关键字
    • continue:跳出当前循环,立即进入下一个循环
    • break:终止当前循环
  • try()catch{}
  • 三元操作符
    • a>b?a:b
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

contract Logic {
  bool ifFlag = true;
  uint32[] raffleIds;
  uint256 executeNumber;

  function IfLogic() external view returns (bool) {
    if (ifFlag) {
      return (true);
    } else {
      return (false);
    }
  }

  function quicksort(uint256[] memory a)
  public
  pure
  returns (uint256[] memory)
  {
    uint256 len = a.length;
    for (uint256 i = 1; i < len; i++) {
      uint256 key = a[i];
      uint256 j = i;
      while (j >= 1 && key < a[j - 1]) {
        a[j] = a[j - 1];
        j--;
      }
      a[j] = key;
    }
    return a;
  }

  // Don't write loops that are unbounded as this can hit the gas limit, causing your transaction to fail.
  function forExecuteRaffle(uint256 count) external {
    uint256 length = raffleIds.length;
    uint256 ncount = executeNumber + count >= length
      ? length
      : executeNumber + count;
    uint256 temp = executeNumber;
    executeNumber = ncount;
    for (uint256 i = temp; i < ncount; i++) {
      // do something
    }
  }

  // Don't write loops that are unbounded as this can hit the gas limit, causing your transaction to fail.
  function whileExecuteRaffle(uint256 count) external {
    uint256 length = raffleIds.length;
    uint256 ncount = executeNumber + count >= length
      ? length
      : executeNumber + count;
    uint256 temp = executeNumber;
    executeNumber = ncount;
    while (temp < ncount) {
      // do something
    }
  }

  // do something first, then do statement check.
  function dowhileCheck(uint256 _number) public pure returns (uint256) {
    do {
      _number += 1;
    } while (_number < 15);
    return _number; //_number+1
  }
}

ABI 编码

abi.encode

  • Encode 按照传参顺序编码合约函数传参,传参被编码成 256bit(32bytes), 不满256bit的参数通过前向或后向补0
  • 静态数据直接编码
  • 动态数据由于动态长度的变化,编码数据分为三部分

静态传参编码规则:

  • 静态参数(int,uint,address,bool,bytes-n,定长数组,只包含静态参数的结构体)高位补0,编码成256bits.
  • 结构体作为 tuples, 按照数据定义顺序直接依次编码
  • 枚举类型占位uint8,会高位补0直到满足 32 byets
  • 自定义的类型按照原类型编码

动态传参编码规则:

  • 动态参数编码(string,bytes,不定长数组,包含动态参数的结构体)分三部分:
    • 第一部分的 256bits(32bytes) 表明 offset,calldata 的起始位置
    • 第二部分的 256bits(32bytes) 表明 length,calldata 中动态参数的长度
    • 第三部分就是真实传参数数据,不满 256bit 的在补0
      • 静态类型高位补0
      • bytes,string类型的参数,低位补0

Examples-String:

play(string) // play(“Eze”)

传参编码的规则:

  1. 函数需要动态类型 string 传参
  2. 按照传参顺序,第一行直接编码动态数组 offset, 之后紧接动态数据大小,(string数据低位补0)

Examples-动态数组:

transfer(uint[], address)// [5769, 14894, 7854], 0x1b7e1b7ea98232c77f9efc75c4a7c7ea2c4d79f1

传参编码的规则:

  1. 函数需要两个传参,第一个为动态数组,第二个为静态地址
  2. 按照传参顺序,第一行先编码动态数组 offset, 动态数据大小为3,内部为静态类型(高位补0)
  3. 第二行编码静态数据

Examples-静态参数结构体:

  1. 带结构体传参的函数选择器和结构体内部参数类型相关
  2. 函数需要两个传参,第一个为全是静态参数的结构体,第二个为静态地址
  3. 按照传参顺序,从第一行开始,直接按照结构体内部参数的定义顺序编码内部参数,每个参数补位 256 bit
  4. 静态地址编码到全部结构体参数后面
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract C {
  struct RareToken {
    uint256 n;
  }
// `foo((uint256),address)`
  function foo(RareToken memory point, address addr) external pure {
    //...
  }

  function getSelector(string calldata str)
  external
  pure
  returns (bytes4, bytes4)
  {
    return (
      bytes4(this.foo.selector),
      bytes4(keccak256(abi.encodePacked(str))) //foo((uint256),address)
    );
  }
}

Examples-动态参数结构体:

    struct RareToken {
    string str;
    uint128 m;
    uint128 n;
}

    function foo(RareToken memory point, address addr) external pure {
        //...
    }
  1. 带结构体传参的函数选择器和结构体内部参数类型相关:foo((string,uint128,uint128),address)
  2. 函数需要两个传参,第一个为是包含动态参数的结构体,第二个为静态地址
  3. 按照传参顺序,从第一行开始,编码结构体参数的 offset
  4. 第二行编码静态地址 addr
  5. 结构体数据从 offset 开始,再次按照数据编码模式进行编码
    1. 结构体按照顺序存储 string, uint128, uint128
    2. 第一行编码 string 参数的存储 offset
    3. 第二三行编码下面的静态参数
    4. 之后开始编码 srting 传参(数据低位补0)
0x5c325b3d [“Eze”,12,23] 5b38da6a701c568545dcfcb03fcb875f56beddc4
0000000000000000000000000000000000000000000000000000000000000040 //动态结构体类型数据的起始位置
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 // 静态数据,存储address
0000000000000000000000000000000000000000000000000000000000000060 //结构体数据动态类型的起始位置
000000000000000000000000000000000000000000000000000000000000000c //结构体静态数据1
0000000000000000000000000000000000000000000000000000000000000017 //结构体静态数据2
0000000000000000000000000000000000000000000000000000000000000003 //结构体数据动态类型的数据长度
4578650000000000000000000000000000000000000000000000000000000000 //结构体数据动态数据

Examples-静态参数的固定大小数组,传参编码的规则:

  1. 静态参数的固定大小数组作为静态参数编码variables.md
  2. 按照传参顺序,直接依次编码静态数据传参
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Pair {
    uint256 x;
    address addr;
    string name;
    uint256[2] array;

    constructor() payable {
        x = 10;
        addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
        name = "0xAA";
        array = [5, 6];
    }

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

0x 000000000000000000000000000000000000000000000000000000000000000a //x = 10

0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c71 // address

00000000000000000000000000000000000000000000000000000000000000a0 // offset = 160, 表示数据从当前 data 的 第160位 开始读取

0000000000000000000000000000000000000000000000000000000000000005 // array[0]

0000000000000000000000000000000000000000000000000000000000000006 // array[1]

0000000000000000000000000000000000000000000000000000000000000004 // 表明数据 length = 4

3078414100000000000000000000000000000000000000000000000000000000

Examples-动态参数的固定大小数组,传参编码的规则:

  1. 动态参数的固定大小数组作为动态参数编码
  2. 按照传参顺序,依次编码各自参数的 offset,之后从 offset 开始存储真实数据的大小和数据
plays(string[2])//play(["Eze","Sunday"])

非固定大小的数组传参string[],需要将数组大小一起编码:

Examples-嵌套数组的编码,传参编码的规则:

  • 直接按照参数顺序,按照动态数组的编码规则依次进行编码

abi.encodePacked

  • 在数据顺序上进行最低空间编码,省略编码中的填充0
  • 动态类型数据只保留数据,不填充长度字段
    • keccak256(abi.encodePacked(“a”, “b”, “c”)) == keccak256(abi.encodePacked(“a”, “bc”)) == keccak256(abi.encodePacked(“ab”, “c”))
  • 当数据不需要和合约交互,只是用来查看数据的情况下就可以使用 encodepacked 节省数据空间
  • encodePacked 不能编码 structnextedArray(以及包含多维数组的map)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Pair {
    uint40 x;
    address addr;
    string name;
    uint8[2] array;
    Tree tree;
    uint8[][]loc;
    mapping(uint256=>uint8[][])nextArrayMap;

    struct Tree {
        uint256 leaves;
        uint256 age;
    }

    constructor() payable {
        x = 10;
        addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
        name = "0xAA";
        array = [5, 6];
        tree = Tree({leaves: 100, age: 1000});
        loc[0].push(6);
        loc[0].push(7);
        nextArrayMap[0]= loc;
    }

    function abiEncodePacked()
    public
    view
    returns (bytes memory encodeData, bytes memory encodePackedData)
    {
        encodeData = abi.encode(x, addr, name, array);
        encodePackedData = abi.encodePacked(x, addr, name, array,nextArrayMap,tree,loc);
    }
}

0x000000000a //uint40 40bit

7a58c0be72be218b41c608b7fe7c5bb630736c71

30784141

0000000000000000000000000000000000000000000000000000000000000005

0000000000000000000000000000000000000000000000000000000000000006

abi.decode

解码 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]));
    }

Preference

https://www.rareskills.io/post/abi-encoding

Solidity参数

数值数据类型占位存储

TypeBit
boolean8
uint8/int8/bytes18
uint32/int32/bytes432
uint128/int128/bytes16128
uint256/int256/bytes32256
address160
enum8
  • slot 存储位置和合约状态变量声明顺序相关,不满栈宽 256bit 高位补到同一个 slot
contract AddressVariable {
    address owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    // new
    bool Boolean = true;
    uint32 thirdvar;
}
  • 其中,owner 占位 160 bit, Boolean 占位 8 bit, thirdvar 占位 32 bit
  • 三个变量按照证明顺序,高位编码到同一个slot存储
  • 此时,slot 剩余 56 bit,下个声明变量的类型如果超出 56 bit,会顺延到 下一个 slot 存储

contract AddressVariable {
    address owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    bool Boolean = true;
    uint32 thirdVar;
    // new
    address admin = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
}

Solidity参数

动态数据类型存储

Mapping数据存储

  • Mapping value 数据的存储位置和声明顺序 baseSlot 以及 key 值相关
    • 直接聚合 bytes32(Key)bytes32(baseSlot)
    • 哈希聚合结果,找出 mapping value 的真实数据存位置

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

contract MyMapping {
  uint256 a; // storage slot 0
  uint256 b; // storage slot 1
  uint256 c; // storage slot 2
  uint256 d; // storage slot 3
  uint256 e; // storage slot 4
  uint256 f; // storage slot 5
  mapping(address => uint256) private balance; // storage slot 6
  mapping(string => uint256) private strbalance; // storage slot 7

  constructor() {
    strbalance["aaa"] = 9; // RED
    balance[address(0x1)] = 7;
  }

  //*** NEWLY ADDED FUNCTION ***//
  function getStorageSlot(address _key)
  public
  pure
  returns (uint256 balanceMappingSlot, bytes32 slot)
  {
    assembly {
    // `.slot` returns the state variable (balance) location within the storage slots.
    // In our case, balance.slot = 6
      balanceMappingSlot := balance.slot
    }

    slot = keccak256(
      abi.encode(
        bytes32(abi.encode(_key)),
        bytes32(abi.encode(balanceMappingSlot))
      )
    );
  }

  function getValue(address _key)
  public
  view
  returns (bytes32 slot, uint256 value)
  {
    // CALL HELPER FUNCTION TO GET SLOT

    (, slot) = getStorageSlot(_key);

    assembly {
    // Loads the value stored in the slot
      value := sload(slot)
    }
  }

  //*** NEWLY ADDED FUNCTION ***//
  function getStringStorageSlot(string memory key)
  public
  pure
  returns (uint256 balanceMappingSlot, bytes32 slot)
  {
    assembly {
    // `.slot` returns the state variable (strbalance) location within the storage slots.
      balanceMappingSlot := strbalance.slot
    }
    slot = keccak256(
      abi.encodePacked(
        abi.encodePacked(key),
        bytes32(abi.encode(balanceMappingSlot))
      )
    );
  }

  function getStringValue(string memory _key)
  public
  view
  returns (bytes32 slot, uint256 value)
  {
    // CALL HELPER FUNCTION TO GET SLOT

    (, slot) = getStringStorageSlot(_key);

    assembly {
    // Loads the value stored in the slot
      value := sload(slot)
    }
  }
}

嵌套Mapping数据存储

  • Mapping(key=>Mapping(…))
  • 多重嵌套 mappingslot 存储规则
    • 每层的 key 作为下层的 baseSlot 值参与计算
    • 最外层的 key 和参数声明顺序相关

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

contract MyNestedMapping {
    uint256 a; // storage slot 0
    uint256 b; // storage slot 1
    uint256 c; // storage slot 2
    uint256 d; // storage slot 3
    uint256 e; // storage slot 4
    uint256 f; // storage slot 5
    mapping(address => mapping(uint256 => uint256)) public balance; // storage slot 6

    constructor() {
        balance[0x5B38Da6a701c568545dCfcB03FcB875f56beddC4][123] = 9; // RED
    }

    //*** NEWLY ADDED FUNCTION ***//
    function getStorageSlot(address key1, uint256 key2)
        public
        pure
        returns (uint256 balanceMappingSlot, bytes32 slot)
    {
        assembly {
            // `.slot` returns the state variable (balance) location within the storage slots.
            // In our case, balance.slot = 6
            balanceMappingSlot := balance.slot
        }

        bytes32 slot1 = keccak256(
            abi.encode(
                bytes32(abi.encode(key1)),
                bytes32(abi.encode(balanceMappingSlot))
            )
        );
        slot = keccak256(abi.encode(bytes32(abi.encode(key2)), slot1));
    }

    function getValue(address key1, uint256 key2)
        public
        view
        returns (bytes32 slot, uint256 value)
    {
        // CALL HELPER FUNCTION TO GET SLOT

        (, slot) = getStorageSlot(key1, key2);

        assembly {
            // Loads the value stored in the slot
            value := sload(slot)
        }
    }

    function convert(string memory key) internal pure returns (bytes32 ret) {
        require(bytes(key).length <= 32);

        assembly {
            ret := mload(add(key, 32))
        }
    }
}

Solidity参数

动态数据类型存储

Arrays数据存储

定长数组

  • 定长数组作为固定大小的参数
  • 数据存储按照静态类型依次 slot 存储
  • 定长数组不和其他参数共享 slot
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;

contract MyFixedUint256Array {
    uint256 num; // storage slot 0

    uint64[3] myArr = [
        4, // storage slot 1
        9, // storage slot 1
        2 // storage slot 1
    ];
    uint64 next = 5; //storage slot 2
    uint128[3] bigArr = [
        4, // storage slot 3
        9, // storage slot 3
        2 // storage slot 4
    ];
    uint64 bnext = 5; //storage slot 5

    function getValue(uint256 index) public view returns (uint256 value) {
        // CALL HELPER FUNCTION TO GET SLOT
        assembly {
            // Loads the value stored in the slot
            value := sload(index)
        }
    }
}

不定长数组–arrays[]

  • 不定长数据在合约编译时无法确认数据 size,所以在 baseSlot 存储当前参数的 size
  • 不定长数组具体数据的起始位置和 baseSlot 相关:keccak256(baseSlot)
  • 不定长数组的数据依次从起始位置开始入栈存储
  • 不定长数组的数据按照数值类型补位存储,slot 进行高位补足存储数组参数
  • 数值长度超出 256bit 后,顺延至下一个 slot 存储 (index += 1)
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;

contract MyDynArray {
  uint256 private someNumber; // storage slot 0
  address private someAddress; // storage slot 1
  uint32[] private myDynamicArr = [3, 4, 5, 9, 7]; // storage slot 2

  function getSlot(uint256 _index) public pure returns (uint256 slot) {
    uint256 baseSlot;
    assembly {
    // `.slot` returns the state variable (balance) location within the storage slots.
    // In our case, balance.slot = 6
      baseSlot := myDynamicArr.slot
    }
    slot = uint256(keccak256(abi.encode(baseSlot))) + _index;
  }

  function getSlotValue(uint256 slot) public view returns (bytes32 value) {
    assembly {
      value := sload(slot)
    }
  }
}

  • 数据类型变成 uint64[] private myDynamicArr = [3, 4, 5, 9, 7]; // storage slot 2:
    • 一个 slot 最多存储 4 个数组参数
    • 因此,起始位置 index=0slot 能够存储 3,4,5,9 四个参数
    • 剩余参数顺眼至下一个 slot
    • 因此,起始位置 index=1slot 存储 7

多维不定长数组

  • array[][]… uint64[][] private nestedDynamicArray = [[2, 9, 6, 3, 2], [7, 4, 8, 10, 2]];
  • baseSlot 存储当前数组大小,示例大小为 2
  • 进一步确认内部嵌套的数组大小,存储内部数组大小的 slot_array_size 和 数组 index 相关:slot_array_size = keccak256(baseSlot)+ index
  • 嵌套数组内部的数据 slot_array_data_locslot_array_size 以及内部 internal_index 相关:slot_array_data_loc = keccak256(slot_array_size)+ internal_index
  • 嵌套数组内部的数据存储按照单维存储规则
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;

contract MyNestedArray {
  uint256 private someNumber; // storage slot 0

  // Initialize nested array
  uint64[][] private nestedDynamicArray = [[2, 9, 6, 3, 2], [7, 4, 8, 10, 2]]; // storage slot 1

  function getArrSizeLoc_BaseSlot_Index(uint256 index)
  public
  pure
  returns (uint256 slot)
  {
    uint256 baseSlot;
    for (uint256 i; i <= index; i++) {
      if (i == 0) {
        assembly {
        // `.slot` returns the state variable (balance) location within the storage slots.
        // In our case, balance.slot = 6
          baseSlot := nestedDynamicArray.slot
        }
      }
      slot = uint256(keccak256(abi.encode(baseSlot))) + index;
    }
  }

  function getArrDataLoc_SlotLoc_InternalIndex(uint256 locslot, uint256 index)
  public
  pure
  returns (uint256 slot)
  {
    slot = uint256(keccak256(abi.encode(locslot))) + index;
  }

  function getSlot(
    uint256 baseSlot,
    uint256 _index1,
    uint256 _index2
  ) public pure returns (bytes32 _finalSlot) {
    // keccak256(baseSlot) + _index1
    uint256 _initialSlot = uint256(keccak256(abi.encode(baseSlot))) +
          _index1;

    // keccak256(_initialSlot) + _index2
    _finalSlot = bytes32(
      uint256(keccak256(abi.encode(_initialSlot))) + _index2
    );
  }

  function getSlotValue(uint256 _slot) public view returns (uint256 value) {
    assembly {
      value := sload(_slot)
    }
  }

  function addArray() external {
    nestedDynamicArray.push([22, 6, 99, 14]);
  }
}

数据:[[2, 9, 6, 3, 2], [7, 4, 8, 10, 2]],Array[array1,array2]

动态数据类型存储

说明slotValue
baseSlot12
array1
存储size的slot,s1
keccack(1)+0=
80084422859880547211683076133703299733277748156566366325829078699459944778998
5
array2
存储size的slot,s2
keccack(1)+1=
80084422859880547211683076133703299733277748156566366325829078699459944778999
5
array1
存储index0的数据slot,d1
keccack(s1)+0=
82253526175936117417672031222849803842933200219522072251142807856800200228130
0x0000000000000003000000000000000600000000000000090000000000000002
array1
存储index1的数据slot,d2
keccack(s1)+1=
82253526175936117417672031222849803842933200219522072251142807856800200228131
0x0000000000000000000000000000000000000000000000000000000000000002
array2
存储index0的数据slot,d3
keccack(s2)+0=
106053296617608346790393806727882046642653284128270527600775845709961105489201
0x000000000000000a000000000000000800000000000000040000000000000007
array2
存储index1的数据slot,d4
keccack(s2)+1=
106053296617608346790393806727882046642653284128270527600775845709961105489202
0x0000000000000000000000000000000000000000000000000000000000000002

Solidity参数

动态数据类型存储

String

  • string 作为动态类型:
    • string(<31bytes),baseSlot 存储 string以及size(len(string) * 2), string 中每个字符占据两位
    • string, string data 编码在高位,size 编码在低位

  • string(>31bytes), baseSlot 仅存储 size(len(string) * 2 + 1)
  • string data 存储的起始 slot = keccak256(baseSlot),依次顺延存储

size

  • 长/短 stringsize 计算方式不同的原因在于让字节码更好的区分 string 的长短
  • stringsize = len(string)*2 表示偶数,因此最后一位一定是 0
  • string size = len(string)*2+1 表示奇数,最后一位一定是 1
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;

contract ModMethod {
    // Gas cost: 326
    function isEvenMod(uint256 num) public pure returns (bool x) {
        x = (num % 2) == 0;
    }

    // Gas cost: 272
    function isEvenAnd(uint256 num) public pure returns (bool x) {
        x = (num & 1) == 0;
    }
}

Bytes

  • bytes 动态数组 slot 存储规则和 string 一致
  • bytes0x 固定大小数组 和 array 固定数组存储规则一致

Solidity参数

动态数据类型存储

Struct

  • 结构体内部数据直接从 baseSlot 依次按照类型存储

Slot 存储读取

  • sload读取当前 slot 位置的 value
  • sstore更新当前 slot 位置的 value,
  • sloadsstore 将参数全部作为 bytes32 处理,更节省 gasFee
  • .slot 返回当前参数的 baseSlot

读取slot数据

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

contract slotLoc {
  uint256 private someNumber = 5; // storage slot 0
  struct Payment {
    address payee;
    uint128 payId;
    uint256 payPrice;
  }
  Payment private payment =
  Payment(
    address(0x1), // storage slot 1
    12345, // storage slot 2
    22 // storage slot 3
  );
  address private someAddress = address(0x2); // storage slot 4
  uint32[] private myDynamicArr = [3, 4, 5, 9, 7]; // storage slot 5

  function getSlot()
  public
  pure
  returns (
    uint256 numslot,
    uint256 paymentslot,
    uint256 addressslot
  )
  {
    assembly {
    // `.slot` returns the state variable (balance) location within the storage slots.
    // In our case, balance.slot = 6
      numslot := someNumber.slot
      paymentslot := payment.slot
      addressslot := someAddress.slot
    }
  }

  function getSlotValue(uint256 slot) public view returns (bytes32 value) {
    assembly {
      value := sload(slot)
    }
  }

  function sstore_x(uint256 newval) public {
    assembly {
      sstore(someNumber.slot, newval)
    }
  }
}

Preference

https://www.rareskills.io/post/solidity-dynamic

User Defined Types

  • Type xx is xxx
  • 用户定义类型别名,通过 wrap/unwrap 包装和解包装
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// Code copied from optimism
// https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/dispute/lib/LibUDT.sol

type Duration is uint64;

type Timestamp is uint64;

type Clock is uint128;

library LibClock {
    function wrap(Duration _duration, Timestamp _timestamp)
        internal
        pure
        returns (Clock clock_)
    {
        assembly {
            // data | Duration | Timestamp
            // bit  | 0 ... 63 | 64 ... 127
            clock_ := or(shl(0x40, _duration), _timestamp)
        }
    }

    function duration(Clock _clock) internal pure returns (Duration duration_) {
        assembly {
            duration_ := shr(0x40, _clock)
        }
    }

    function timestamp(Clock _clock)
        internal
        pure
        returns (Timestamp timestamp_)
    {
        assembly {
            timestamp_ := shr(0xC0, shl(0xC0, _clock))
        }
    }

    function unwrap(Clock clock_)
        internal
        pure
        returns (Duration _duration, Timestamp _timestamp)
    {
        _duration = duration(clock_);
        _timestamp = timestamp(clock_);
    }
}

contract userDefinedValue {
    function wrap_uvdt() external view returns (Clock clock) {
        // Turn value type into user defined value type
        Duration d = Duration.wrap(1);
        Timestamp t = Timestamp.wrap(uint64(block.timestamp));
        // Turn user defined value type back into primitive value type
        // uint64 d_u64 = Duration.unwrap(d);
        // uint64 t_u54 = Timestamp.unwrap(t);
        clock = LibClock.wrap(d, t);
    }

    function unwrap_uvdt(Clock clock)
        external
        pure
        returns (Duration d, Timestamp t)
    {
        (d, t) = LibClock.unwrap(clock);
    }
}

函数

函数修饰符

function <function name> (<parameter types>) {internal|external|public|private} [pure|view|payable|virtual|override|Modifier] [returns (<return types>)]

  • internal:未公开的函数,只能在合约内部使用,不能通过 abi 直接/外部调用,允许在继承的子合约中使用
  • external:公开的函数,可以通过 abi 直接调用,也可以在本合约内通过 this.func 调用,允许在继承的子合约中使用
  • public:公开的函数,可以通过 abi 直接调用,允许在继承的子合约中使用
  • private: 未公开的函数,只能在合约内部使用,不允许通过 abi 直接调用,不允许在继承的子合约中使用
  • pure: 只读,函数只能读取局部变量
  • view: 只读,函数可以读取状态变量、局部变量、全局变量
  • payable: payable 修饰符表明函数支持接收 NativeToken(msg.value!=0)
  • virtual: 虚函数,表明函数允许重载修改内部逻辑, 多用于接口合约
  • override: 重载函数,重新定义函数内部逻辑,但是函数 selector 必须保持一致(函数名称、函数参数类型和数量)
  • Modifier: 修饰器,内部定义条件判断, 允许在继承的子合约中使用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract parentOne {
    uint256 public oneValue;

    function OneInternal() internal virtual {
        oneValue += 1;
    }

    function OneExternal() external virtual {
        oneValue += 2;
    }

    function OnePublic() public virtual {
        oneValue += 3;
    }

    function OnePrivate() private {
        oneValue += 4;
    }
}

contract functionsChecker is parentOne {
    function addOne() external {
        OneInternal();
    }

    // function addTwo() external {
    //     this.OneExternal();
    // }

    function addThree() external {
        super.OnePublic(); // super调用最近继承者合约的内部函数, +3
    }

    // function addFive() external {
    //     OnePublic(); //调用本合约的函数,+ 5
    // }

    function OnePublic() public virtual override {
        //重写函数,修改函数逻辑
        oneValue += 5;
    }

    //receive() external payable {}
}

函数

构造函数

  • 构造函数用于初始化合约参数
    • 允许传参,初始化状态变量
    • 支持 payable 修饰,允许合约部署时 msg.value!=0
  constructor(parameters)[payable] {
   // to-do
  }
  • 构造函数编码在 initCode,并不会存储上链,仅用来初始化合约参数
    • 构造函数的代码不会存储上链,但是构造函数的传参会编码到合约代码
    • 如果构造函数内部逻辑为空,那么链上可执行的合约代码和该函数无关
  • 继承合约时,需要传参初始化继承的父合约的构造函数
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

contract tree {
    uint256[] Ids;

    constructor(uint256 id) {
        Ids.push(id);
    }
}

contract leaf is tree {
    constructor(uint256 id) tree(id) {}
}

函数修饰器

modifier 修饰符用于判断合约方法的执行前置条件

  • 将函数内部的语句放置在 modifier 函数的 - 中执行判断
  • 允许继承使用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract modofier {
  address owner; //slot 0
  modifier onlyOwner() {
    require(msg.sender == owner, "Not_Owner"); // 判断 slot0存储的值和当前发送者的地址是否一致
    _;
  }
}

contract modofierContract is
modofier // 继承父合约的状态变量,private变量继承后无法更新状态值
{
  constructor() {
    address _addr = msg.sender;
    assembly {
      sstore(owner.slot, _addr)
    }
  }

  function getValue(uint256 slot) external view returns (address addr) {
    assembly {
      addr := sload(slot)
    }
  }

  function changeOwner(address _newowner) public onlyOwner {
    owner = _newowner;
  }
}

函数选择器

selector

  • 选择器和 函数名、函数参数 相关,和函数&参数的修饰符无关, bytes4(keccak256(abi.encodePacked(functionName)))
    • 计算选择器时,函数参数之间无空格
  • 使用4个字节的函数选择器降低 msg.datasize (函数名称可以无限长,选择器就是 4bytes)
  • 函数选择器不会和 fallback() 进行匹配,只有在全部的功能函数没匹配上的时候,才会调用 fallback() 函数
  • 因此,solidity 支持同名不同参数的函数, 因为函数选择器的 hash 值不一样

selector外部调用

  • abi.encodePacked
  • abi.encodeWithSelector
  • abi.encodeWithSignature
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract SelectorTest {
  uint256 public x;
  event Sig(bytes4 sig);

  function foo(uint256 num) public {
    emit Sig(msg.sig); //this.foo.selector
    x += num;
  }

  function func(uint256 num) external {
    x += num;
  }

  function func(uint256 num, bool flag) external {
    if (flag) {
      x += num;
    }
  }

  function getSelectorOfFoo() external pure returns (bytes4) {
    return this.foo.selector; // 0xc2985578
  }
}

contract CallFoo {
  event Sig(bytes4 sig);

  function callFooLowLevel(SelectorTest _contract, uint256 num) external {
    //  _contract.foo(num);
    // bytes4 fooSelector = 0xc2985578;
    emit Sig(msg.sig); // this.callFooLowLevel.selector
    bytes4 fooSelector = SelectorTest.foo.selector; //bytes4(keccak256("foo(uint256)"))
    (bool ok, ) = address(_contract).call(
      abi.encodePacked(fooSelector, num)
    );
    // | (bool ok, ) = address(_contract).call(abi.encodeWithSelector(fooSelector, num));
      // | (bool ok, ) = address(_contract).call(abi.encodeWithSignature("foo(uint256)", num));
    require(ok, "call failed");
  }
}

Reference

函数在线计算选择器

函数选择器数据库

https://www.rareskills.io/post/function-selector

转账

  • receive() external payable {},函数用于接收 NativeToken (EOA直接转账, msg.value !=0 )
  • fallback()external payable{} ,函数缺省情况(包括转账后没有 receive() 方法)
  • send 执行转账
    • 传递 2300gas
    • 转账返回 boolean
    • 转账失败不会 revert 交易,因此,需要判断转账结果
  • transfer
    • 传递 2300gas
    • 转账失败的话,整笔交易回滚
  • call的转账
    • 默认会发送 63/64 gas
    • 返回 booldata
      • bool 表示转账是否成功
      • data 返回执行调用的结果
    • 交易失败的话不会回滚。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract receiver {
  constructor() {}

  event ReceiveReceived(address Sender, uint256 Value);
  event FallbackReceived(address Sender, uint256 Value);

  receive() external payable {
    emit ReceiveReceived(msg.sender, msg.value);
  }

  fallback() external payable {
    emit FallbackReceived(msg.sender, msg.value);
  }
}

contract payer {
  constructor() payable {}

  function sendValue(address payable recipient, uint256 amount) external {
    bool success = recipient.send(amount); //ReceiveReceived
    if (!success) {
      revert("Send Trans Failure");
    }
  }

  function transferValue(address payable recipient, uint256 amount) external {
    recipient.transfer(amount); //ReceiveReceived
  }

  function callSendValue(
    address payable recipient,
    uint256 amount,
    bytes memory _data
  ) external {
    (bool success, ) = recipient.call{value: amount}(_data); //ReceiveReceived
    if (!success) {
      revert("call_send_value_failure");
    }
  }

  // 11053 gas cost
  function safeTransferNativeToken(
    address to,
    uint256 amount,
    uint256 txGas
  ) external {
    bool success;
    /// @solidity memory-safe-assembly
    assembly {
    // Transfer the ETH and store if it succeeded or not.
    //success := call(gas(), to, amount, 0, 0, 0, 0)
      success := call(txGas, to, amount, 0, 0, 0, 0)
    }
    require(success, "ETH_TRANSFER_FAILED");
  }
}

异常捕获

Solidity 将异常情况分为三类:前置(require)、后置(assert)、抛出异常(revert): 三种异常通过 try-catch 捕获处理

require 前置判断

  • require(condition,"ErrorMsg“), which throws if the condition is false
    • 条件不满足时就会抛出异常,输出异常字符串(ErrorMsg)
    • gas 花销和描述异常的字符串的长度正相关
        require(
            address(this).balance >= value,
            "Address: insufficient balance for call"
        );

assert 后置判断

  • assert(condition), which throws if the condition is false
  • 一般作为后置条件判断,但是无法抛出具体的异常信息
        assert (balanceAfter < balanceBefore);

revert

  • revert() | revert(ErrorMsg) | revert Error()
    • 可以通过 revert() 直接抛出异常
    • 异常内支持添加异常信息 revert(ErrorMsg)
    • 允许抛出自定义的 Error(), 通过 revert Error() 抛出
error TransferNotOwner(); // 自定义error
function transferOwner1(uint256 tokenId, address newOwner) public {
    if(_owners[tokenId] != msg.sender){
        revert TransferNotOwner();
    }
     _owners[tokenId] = newOwner;
}

try catch

Lower-level

lower-level的调用在EVM层面,直接返回 boolean,bytes,让用户自行捕获异常:

  • 外部调用的合约函数执行出现了 revert
  • 外部调用的合约函数执行了非法逻辑(/0,out-of-bound
  • 外部调用的合约函数 out-of-gas

异常捕获

try external function calls and contract creation calls{}catch

  • try 用户捕获外部函数调用的异常
  • 不可用于 selector 调用的异常捕获
  • catch 匹配异常
    • Panic(errorCode) via assert
      • 0x00,编译器错误
      • 0x01,assert 报错
      • 0x11,数字越界错误
      • 0x12,/0
      • 0x21,转化枚举类型时:传参负数或越界
      • 0x22,访问错误编码的bytes数组
      • 0x31,空数组pop
      • 0x32,数组越界
      • 0x41,申请内存超额或数组太大
      • 0x51,访问局部变量
      • 返回数据: bytes4(keccak256(”Panic(uint256)”))) + encode_uint256
    • Error via require|revert
      • require(bool) = if(bool){revert()},直接 revert(),返回数据为空
      • require(bool, string) = if(bool){revert(string)},返回数据是 Error(string) 的编码
      • require(bool, UserCustomError()) = if(bool){revert UserCustomError()},报用户自定义错误,返回数据是 bytes4(keccak256("UserCustomError()")))
function callContractB() external view {
  try functionFromAnotherContract() {
    //<-- Handle the success case if needed
  } catch Panic(uint256 errorCode) {
    //<-- handle Panic errors
  } catch Error(string memory reason) { //revert(string), require(false, “reason”)
    //<-- handle revert with a reason
  } catch (bytes memory lowLevelData) { //revert UserCustomError(),require(bool, UserCustomError())
    //<-- handle every other errors apart from Panic and Error with a reason
    // revert without a message
    if (lowLevelData.length == 0) {
      console.log("revert without a message occured");
    }

    // Decode the error data to check if it's the custom error
    if (bytes4(abi.encodeWithSignature("CustomError(uint256)")) ==bytes4(lowLevelData)
    ) {
      // handle custom error
      console.log("CustomError occured here");
    }
  }
}

Preference

https://www.rareskills.io/post/try-catch-solidity

import

  • import 导包在声明版本号后,在合约代码前 import {contractName} from '...'
  • 导入合约后,相当于引入了完整的合约文件

import 导包

  1. 导入本地文件 >import {Address} from ‘./Address.sol’;
  2. 从网页导入

import {Address} from “https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/refs/heads/master/contracts/utils/Address.sol”;

  1. 通过npm 本地包导入

import “@openzeppelin/contracts/utils/Address.sol”;

包内函数

  • 直接使用:通过合约名直接调用引入的合约函数
  • 继承使用:除library外的合约,继承后可以直接使用包内函数、状态变量
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {IERC20} from "https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/refs/heads/master/contracts/token/ERC20/IERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

contract sendValue {
    function tokenAllowance(
        address token,
        address owner,
        address spender
    ) public view returns (uint256) {
        return IERC20(token).allowance(owner, spender); //Interface contracts
    }

    function transferValue(address beneficiary, uint256 value) public payable {
        Address.sendValue(payable(beneficiary), value); //Library contracts
    }

    function allowance(address owner, address spender)
    external
    view
    returns (uint256)
    {}
}

创建合约

合约通过关键字 CREATECREATE2,new 创建

CREATE

  • 新合约地址 address = keccak256(rlp([sender_address,sender_nonce]))[12:]
    • 同一条链上的账户地址 nonce 递增,因此账户在相同链上无法部署同样的合约账户
    • 同样地址在不同链上,能过够通过相同 nonce 值部署相同地址的合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract FindSC {
  function contractAddressFrom(address _origin, uint256 _nonce)
  public
  pure
  returns (address)
  {
    bytes memory data;
    if (_nonce == 0x00)
      data = abi.encodePacked(
        bytes1(0xd6),
        bytes1(0x94),
        _origin,
        bytes1(0x80)
      );
    else if (_nonce <= 0x7f)
      data = abi.encodePacked(
        bytes1(0xd6),
        bytes1(0x94),
        _origin,
        uint8(_nonce)
      );
    else if (_nonce <= 0xff)
      data = abi.encodePacked(
        bytes1(0xd7),
        bytes1(0x94),
        _origin,
        bytes1(0x81),
        uint8(_nonce)
      );
    else if (_nonce <= 0xffff)
      data = abi.encodePacked(
        bytes1(0xd8),
        bytes1(0x94),
        _origin,
        bytes1(0x82),
        uint16(_nonce)
      );
    else if (_nonce <= 0xffffff)
      data = abi.encodePacked(
        bytes1(0xd9),
        bytes1(0x94),
        _origin,
        bytes1(0x83),
        uint24(_nonce)
      );
    else
      data = abi.encodePacked(
        bytes1(0xda),
        bytes1(0x94),
        _origin,
        bytes1(0x84),
        uint32(_nonce)
      );
    return address(uint160(uint256(keccak256(data))));
  }
}

CREATE2

creation_code = memory[offset:offset+size]

address = keccak256(0xff + sender_address + salt + keccak256(creation_code))[12:]

  • 通过自定义的 salt合约代码 替换递增的 nonce
    • 合约代码 一般选择合约的 creationCode
    • creationCode 包含完整的合约代码 initCode + runtimeCode,在构造函数发生任何变化后,也会造成合约地址的变化
  • 在相同链上通过相同的 salt合约代码,就可以实现同地址合约的提前使用

New

  • 新合约地址:Contract x = new Contract{value: _value,salt: salt}(params)
  • new 关键字创建新的合约地址
    • value 表明创建合约是是否转账,构造函数需要使用 payable 修饰
    • salt 表明当前新建地址是否采用 CREATE2 关键字
    • constructor parameters 表明新建合约时传递的初始化参数

Solidity Contracts

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

// This is the older way of doing it using assembly
contract CreateNewContract {
    event Create2Created(address addr, bytes32 salt);
    event CreateCreated(address addr);

    // 1. Deploy the contract by CREATE
    function Create(address _owner, uint256 _num) external payable {
        bytes memory bytecode = getBytecode(_owner, _num);
        _create(bytecode, "");
    }

    // 2. Deploy the contract by CREATE2
    function Create2(
        address _owner,
        uint256 _num,
        bytes32 salt
    ) external payable {
        bytes memory bytecode = getBytecode(_owner, _num);
        _create(bytecode, salt);
    }

    function _create(bytes memory bytecode, bytes32 salt) internal {
        address addr;
        if (keccak256(abi.encode(salt)) == keccak256("")) {
            /*
        NOTE: How to call create
        create(v, p, n)
        create new contract with code at memory p to p + n
        and send v wei
        and return the new address
        */
            assembly {
                addr := create(
                    callvalue(), // wei sent with current call
                    // Actual code starts after skipping the first 32 bytes
                    add(bytecode, 0x20),
                    mload(bytecode) // Load the size of code contained in the first 32 bytes
                )

                if iszero(extcodesize(addr)) {
                    revert(0, 0)
                }
            }
            emit CreateCreated(addr);
        } else {
            /*
        NOTE: How to call create2
        create2(v, p, n, s)
        create new contract with code at memory p to p + n
        and send v wei
        and return the new address
        where new address = first 20 bytes of keccak256(0xff + address(this) + s + keccak256(mem[p…(p+n)))
              s = big-endian 256-bit value
        */
            assembly {
                addr := create2(
                    callvalue(), // wei sent with current call
                    // Actual code starts after skipping the first 32 bytes
                    add(bytecode, 0x20),
                    mload(bytecode), // Load the size of code contained in the first 32 bytes
                    salt // Salt from function arguments
                )

                if iszero(extcodesize(addr)) {
                    revert(0, 0)
                }
            }
            emit Create2Created(addr, salt);
        }
    }

    // 3. Deploy the create contract by new
    function NewCreate(address _owner, uint256 _num)
        public
        payable
        returns (address)
    {
        return address(new targetContract{value: msg.value}(_owner, _num));
    }

    // 4. Deploy the create2 contract by new
    function NewCreate2(
        address _owner,
        uint256 _num,
        bytes32 salt
    ) public payable returns (address) {
        // This syntax is a newer way to invoke create2 without assembly, you just need to pass salt
        // https://docs.soliditylang.org/en/latest/control-structures.html#salted-contract-creations-create2
        return
            address(
                new targetContract{salt: salt, value: msg.value}(_owner, _num)
            );
    }

    // Get bytecode of contract to be deployed
    // NOTE: _owner and _num are arguments of the targetContract's constructor
    function getBytecode(address _owner, uint256 _num)
        internal
        pure
        returns (bytes memory)
    {
        bytes memory bytecode = type(targetContract).creationCode;
        return abi.encodePacked(bytecode, abi.encode(_owner, _num));
    }

    // Compute the address of the contract to be deployed
    // NOTE: _salt is a random number used to create an address
    function getAddress(
        address _owner,
        uint256 _num,
        bytes32 salt
    ) external view returns (address) {
        bytes memory _bytecode = type(targetContract).creationCode;
        bytes memory bytecode = abi.encodePacked(
            _bytecode,
            abi.encode(_owner, _num)
        );
        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff),
                address(this),
                salt,
                keccak256(bytecode)
            )
        );

        // NOTE: cast last 20 bytes of hash to address
        return address(uint160(uint256(hash)));
    }
}

contract targetContract {
    address public owner;
    uint256 public num;

    constructor(address _owner, uint256 _num) payable {
        owner = _owner;
        num = _num;
    }

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

contract-creation

读取内存数据

codecopy 从内存中取出数据

  • destOffset: 内存中读取数据的起始位置
  • offset: 待拷贝数据的起始位置
  • size: 拷贝数据的长度 codecopy

Introduction-creation code

  • 合约编译成两部分: <initCode> + <Runtimecode>(part1_runtimeCode + part2_Metadata)

Miniature Contract Examples

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17; // optimizer: 200 runs

contract Minimal {
  // 空 constructor()函数不影响合约部署的字节码,存在或不存在 constructor()函数,字节码都一样
  constructor() payable {}
}

编译后的字节码: 0x6080604052603f8060116000396000f3fe + 6080604052600080fdfea2646970667358221220a4c95008952415576a18240f5049a47507e1658565a8ec11a634c25a9aa17cf164736f6c63430008110033

initCode

initcode-playground

  • initcode 执行 codecopy,栈内数据自栈顶向下依次存在值:00,11,3f,3f
    • 从内存 0index 开始,间隔 0x11=17 开始,跳过 initCode 部分,读取 runtimeCode0x3f=63bytes 数据
  • return 返回内存数据,除 initCode 部分的值,作为合约数据存储上链

payable|Non-payable constructor()

  • payable 修饰的构造函数下的 initCode0x6080604052603f8060116000396000f3fe
  • Non-payable 修饰的构造函数下的 initCode0x6080604052348015600f57600080fd5b50603f80601d6000396000f3fe
    • Non-payableinitCode 更大
    • 构造函数需要执行 msg.value 的判断: 348015600f57600080fd5b50 (12 bytes)
    • <initCode> + <extra 12 byte sequence (payable case)>

Non-payable evm codes

// check the amount of wei that was sent
CALLVALUE
DUP1
ISZERO

// Jump to 0x0f (contract deployment step)
PUSH1 0x0f
JUMPI

// revert if wei sent is greater than 0
PUSH1 0x00
DUP1
REVERT

RuntimeCode

  • RuntimeCode 由两部分组成: <Runtimecode>(part1_runtimeCode + part2_Metadata)
    • part1_runtimeCode 存储真正的合约逻辑代码,进行函数选择器的匹配和处理
    • part2_Metadata 由两部分组成:当前合约编译环境等源数据 + 构造函数传参
  • 在空合约中,part1_runtimeCode 为空,但是 part2_Metadata 源数据不为空

Constructor without parameters

pragma solidity 0.8.17;
contract Runtime {
    address lastSender;
    constructor () payable {}

    receive() external payable {
        lastSender = msg.sender;
    }
}

编译后的字节码:<initCode> + <Runtimecode>(part1_runtimeCode + part2_Metadata) >part1_runtimeCode := 0x608060405236601c57600080546001600160a01b03191633179055005b600080fdfe > >part2_Metadata := 0xa2646970667358221220e9b731ab28726d97cbf5219f1e5eaec508f23254c60b15ed1d3456572547c5bf64736f6c63430008070033a2646970667358221220e9b731ab28726d97cbf5219f1e5eaec508f23254c60b15ed1d3456572547c5bf64736f6c63430008070033

  • part1_runtimeCode
    • msg.data != "", 由于不存在 fallback() 函数,直接 revert
    • msg.value != "", 由于存在 receive() 函数,直接跳转到 receive 函数执行逻辑

Constructor with parameters

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17; // optimizer: 200 runs

contract Minimal {
  // optimizer: 200contract MinimalLogic {
  uint256 private x;
  constructor(uint256 _x) payable {
    x = _x;
  }
}

编译后的字节码:<initCode> + <Runtimecode>(part1_runtimeCode + part2_Metadata) >“Init code”: 0x608060405260405160893803806089833981016040819052601e916025565b600055603d565b600060208284031215603657600080fd5b5051919050565b603f80604a6000396000f3fe“Runtime code (metadata only)“: 0x6080604052600080fdfea26469706673582212204a131c1478e0e7bb29267fd8f6d38a660b40a25888982bd6618b720d4498b6b464736f6c63430008070033“Constructor arguments”:0000000000000000000000000000000000000000000000000000000000000001

  • initCode
    • 构造函数需要一个 address 类型的传参,传参无效的话,直接 revert
    • 初始化传参变量
  • part1_runtimeCode
    • msg.data != "", 由于不存在 fallback() 函数,直接 revert
    • msg.value != "", 由于不存在 receive() 函数,直接 revert
  • part2_Metadata
    • 当前合约编译环境等源数据
    • 构造函数的传参编码到源数据最后

selfdestruct

  • selfdestruct 关键字强制将当前合约余额转给传参地址
  • 自毁关键字下的转账行为是不可控,任何合约内部执行 selfdestruct 后都会把余额强制转给传参地址

Dencun upgrade

Dencun 升级后针对 selfdestruct 的问题进行了修正升级:

  • selfdestruct 交易仅仅将合约余额发送到传参地址, 并且不会清除合约状态和代码
  • 销毁后,不用再次通过 CREATE2 部署相同地址合约,因为原地址的状态和代码还保留在链上
  • 销毁后,任何的外部调用 call|delegateCall 也不会失败
  • 也不会存在代币锁仓的隐患,因为之前合约的逻辑和状态并未清空

Solidity Examples

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

contract Sale {
    uint256 public num;

    function buy() external payable {
        num += 1;
        // _end();
    }

    function _end() public {
        // 仍然保留 num 状态变量的参数
        selfdestruct(payable(msg.sender));
    }

    function bal() external view returns (uint256 baln) {
        baln = address(this).balance;
    }
}

contract caller {
    constructor() payable {}

    function buy(Sale sale) external {
        (bool success, ) = address(sale).call{value: 100 wei}(
            abi.encodeWithSelector(sale.buy.selector)
        );
        if (!success) {
            revert();
        }
    }
}

event事件

Event 事件是EVM上日志的抽象,具有两大特点:

  1. emit 触发事件,可以直接过滤、订阅事件
  2. 经济友好,通过 Event 存储数据,每次存储大概花销 2000gas
event Transfer(address indexed from, address indexed to, uint256 value);

事件Transfer内部参数表示需要记录在链上的变量类型和参数名称

  • 通过 indexed 关键字,单独将事件参数作为一个 topic 进行存储和索引
  • 一个 Event 最多拥有三个 indexed 数据
  • 没有 indexed 关键字修饰的参数作为 data 变量存储在 data 域中,作为参数解析

Golang 解析 event事件

func ParseLogFromReceipet(contract, txhash string) {
    receipet, err := client.TransactionReceipt(context.Background(), common.HexToHash(txhash))
    if err != nil {
       log.Fatalf("Error in get TransactionReceipt: %s", err)
    }
    if receipet.Status == 0 {
       fmt.Printf("Tx has failed\n")
       return
    }
    logs := receipet.Logs
    ParseLogs(logs, contract)
}

func ParseLogs(logs []*types.Log, contract string) bool {
    for _, logData := range logs {
       if logData.Address != common.HexToAddress(contract) {
          continue
       }
       Sender := common.HexToAddress(logData.Topics[1].Hex())
       types, _ := strconv.ParseInt(hexutil.Encode(logData.Topics[2].Bytes())[2:], 16, 64)
       uuid, _ := strconv.ParseInt(hexutil.Encode(logData.Topics[3].Bytes())[2:], 16, 64)
       fmt.Printf("Contract: %s, Sender: %s, types: %d, uuid: %d\n", contract, Sender, types, uuid)
       return true
    }
    return false
}

event topic过滤

同时过滤topic1 + topic2 的 logs

for startBlockHeight < latestblockNum {
		toblock := startBlockHeight + 10000
		if toblock > latestblockNum {
			toblock = latestblockNum
		}
		query := ethereum.FilterQuery{
			FromBlock: big.NewInt(startBlockHeight),
			ToBlock:   big.NewInt(toblock),
			Addresses: []common.Address{common.HexToAddress(contract)},
			Topics:    [][]common.Hash{{common.HexToHash("topic1"), common.HexToHash("topic2")}},
		}
		logs, err := mainnetLPClient.FilterLogs(context.Background(), query)
		if err != nil {
			fmt.Printf("Error in filter logs: error:%v", err)
			return
		}
}

contract-codes

判断是否是合约地址的三种方式

  • msg.sender==tx.origin
  • EXTCODESIZE 读取 code.length
  • EXTCODEHASH 读取 code.hash

msg.sender==tx.origin

  • tx.origin 是当前交易的签名地址
  • msg.sender 是当前 EVM 执行环境中的交易发送地址
  • 对于 EOA 直接发起的的合约交易,合约内部:msg.sender==tx.origin
  • 合约之间外部调用重启 EVM 执行环境的外部调用:msg.sender!=tx.origin

EXTCODESIZE

  • codeSize = 0, 不表示该地址一定是 EOA 地址
    • 有可能是预定义的 create2 地址,未来会部署成为合约
    • 对方在 constructor() 中调用 call 交易,此时合约未存储上链,codeSize = 0
  • selfdestructdencun 升级后并不会清除合约状态和代码,因此 codeSize != 0

Solidity Examples

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

contract TestAddressSize {
    event codeSize(uint256);

    function constructorCodeSize() external {
        uint256 codes;
        address sender = msg.sender;
        assembly {
            codes := extcodesize(sender)
        }
        emit codeSize(codes);
    }

    // 765 gas
    function codesize(address target) public view returns (bool isContract) {
        if (target.code.length == 0) {
            isContract = false;
        } else {
            isContract = true;
        }
    }

    // 779 gas
    function codesizeAssm(address target) public view returns (bool) {
        uint256 size;
        assembly {
            size := extcodesize(target)
        }
        return size != 0;
    }
}

contract onlyConstructor {
    constructor() {
        address addr = 0x10E2fC1dE57DDC788489122151a6c45254D3ba59;
        (bool success, ) = addr.call(
            abi.encodeWithSignature("constructorCodeSize()")
        );
        if (!success) {
            revert();
        }
    }
    // [
    // 	{
    // 		"from": "0x10E2fC1dE57DDC788489122151a6c45254D3ba59",
    // 		"topic": "0x35bbf8dac6652434e49dd256e75001562f8cabc9ab024b4ed3f7826b3ab5a81f",
    // 		"event": "codeSize",
    // 		"args": {
    // 			"0": "0"
    // 		}
    // 	}
    // ]
}

contract afterSelfDestruct {
    function deposit() external payable {}

    // 销毁后并不会影响合约的使用
    // 销毁保留合约的状态的代码
    // 销毁仅仅是强制将合约余额转出
    function kill() external {
        selfdestruct(payable(msg.sender));
    }
}

EXTCODEHASH

  • 返回 keccak256(codes)
  • 如果地址 balance == 0 && codeSize == 0, 返回 bytes32(0)
  • 如果地址 balance != 0 && codeSize == 0, 返回 keccak256(""")=0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
  • 如果地址 codeSize != 0, 返回 keccak256(codes)
  • 全部 预编译合约预存 1wei,因此会返回空值的 hash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract TestAddressSize {
    event codeHash(bytes32);

    function constructorCodeHash() external {
        bytes32 codehashs;
        address sender = msg.sender;
        assembly {
            codehashs := extcodehash(sender)
        }
        emit codeHash(codehashs);
    }

    //  gas
    function codehash(address target) public view returns (bytes32 hash) {
        hash = target.codehash;
    }

    //  gas
    function codesizeAssm(address target) public view returns (bytes32 hash) {
        assembly {
            hash := extcodehash(target)
        }
    }
}

contract onlyConstructor {
    //0x0000000000000000000000000000000000000000000000000000000000000000
    constructor() {
        address addr = 0x16a90f9ec7A46514b47487bDc2F00d11740c3BA0;
        (bool success, ) = addr.call(
            abi.encodeWithSignature("constructorCodeSize()")
        );
        if (!success) {
            revert();
        }
    }
}

contract afterSelfDestruct {
    function deposit() external payable {}

    // before kill: 0xd0516dff3313077772b6176aa83c5ad4da898b2f66e2dd058b612dbace072fbc
    // after kill: 0xd0516dff3313077772b6176aa83c5ad4da898b2f66e2dd058b612dbace072fbc
    // 销毁后并不会影响合约的使用
    // 销毁保留合约的状态的代码
    // 销毁仅仅是强制将合约余额转出
    function kill() external {
        selfdestruct(payable(msg.sender));
    }
}

判断地址是否是合约地址

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

library ContractAddress {
    function isContract(address contractAddress) internal view returns (bool) {
        bytes32 existingCodeHash = contractAddress.codehash;

        // https://eips.ethereum.org/EIPS/eip-1052
        // keccak256('') == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
        return
            existingCodeHash != bytes32(0) &&
            existingCodeHash != 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
    }
}

接口

  • 接口合约定义函数骨架
  • 接口合约通过interface关键字修饰
  • 接口合约不能继承除接口外的其他合约
  • 接口合约不能定义状态变量
  • 接口合约不能包含构造函数
  • 接口函数必须使用external修饰
  • 接口函数不需要virtual修饰,因为继承接口需要实现接口内的全部函数
  • 继承接口合约,必须实现接口内的全部函数
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

interface IERC721 {
    event Getbal(address indexed owner);

    function balanceOf(address owner) external returns (uint256 balance);

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

contract myToken is IERC721 {
    mapping(address => uint256) balances;

    function deposit() public payable {
        balances[msg.sender] += 1000;
    }

    function balanceOf(address owner) external returns (uint256) {
        emit Getbal(owner);
        return balances[owner];
    }

    function ownerOf(uint256 tokenId) external view returns (address owner) {}

    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external pure returns (bytes4) {
        return IERC721Receiver.onERC721Received.selector;
    }
}

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

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

库合约

  • 库合约使用library关键字修饰
  • 库合约作为完整函数的封包使用
  • 库合约不能接收token
  • 库合约不能被继承或继承别的合约
  • 库合约不能存在状态变量
  • 库合约不能定义构造函数
  • 库合约的使用分为两种:
    • 通过 Using library_name for type,此时 type 类型的变量就可以调用库合约内部函数
    • 直接通过 library_name 调用函数

库合约修饰符

  1. 仅带有 Internal 修饰符的库合约函数代码直接被编码到执行合约中,通过 jump 跳转执行
  2. 带有 external 修饰符的库合约,必须先被部署在链上,在将链上的库合约地址 编码到执行合约,执行合约通过 delegateCall 库合约
  3. DelegateCalljump 跳转执行花费更多的 gas
  4. 并且带有 external 函数的库合约必须先部署在链上,也需要额外的合约部署成本

Solidity Library Internal Function Example

  • 仅使用 internal 修饰的 library 合约不需要被部署到区块链上
  • 库合约的内部函数被编码到当前执行合约中,直接 jump 到当前代码执行

Example: ContractsA 使用 库合约 LibInternal 中的 FuncA

编译合约 ContractA

编译库合约 LibInternal

库合约中的 funcA bytecode 被复制编码到 ContractA bytecode

ContractA bytecode 部署到链上

ContractA 调用 FuncA 的方式和自己内部函数一样,执行 selector 匹配 以及jump 跳转

Solidity Library External Function Example

  • 调用任一带有 external 修饰的 library 合约函数,必须先把库合约部署到区块链上
  • 执行合约通过 delegateCall 调用库合约函数中的 external 函数

Example: ContractsB 调用库合约 LibInternal 中的 External 修饰的 FuncA

编译合约 ContractB

编译库合约 LibExternal

LibExternal 首先被部署到链上

ContractB bytecode 中嵌入库合约地址,方便后续执行 delegateCall 调用

ContractB 被部署到链上

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

library Address {
   struct st {
      string name;
      uint256 value;
   }

   function isContract(address account) internal view returns (bool) {
      uint256 size;
      assembly {
         size := extcodesize(account)
      }
      return size > 0;
   }
}

contract StateToStateContract {
   using Address for address;

   function isSC(address _addr) public view returns (bool) {
      return Address.isContract(_addr); //_addr.isContract();
   }
}

preference

Difference of internal and external library

抽象合约

  • 抽象合约介于接口合约和完整合约之间,内部存在未定的函数,不能被直接部署
  • 抽象通过abstract关键字修饰,内部包含至少一个未实现具体功能的函数方法(没有{}
  • 抽象合约是用来被继承后补全使用的base contract

逻辑未定函数

  • 未实现的函数必须使用virtual修饰,支持后续继承合约的override重写
  • 继承函数时,必须继承函数的全部函数、变量、数据结构、修饰器
    • 必须override重写函数,补全抽象合约中未定的函数
    • 不能修改函数[名称、修饰符、返回类型]、传参的[参数类型、参数数量]
    • 允许继承 immutable|constant变量
    • immutable 直接在继承时初始化
    • constant 直接继承使用

Solidity Contracts

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

/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol)
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20 {
  /*//////////////////////////////////////////////////////////////
                               EVENTS
  //////////////////////////////////////////////////////////////*/

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

  event Approval(
    address indexed owner,
    address indexed spender,
    uint256 amount
  );

  /*//////////////////////////////////////////////////////////////
                          METADATA STORAGE
  //////////////////////////////////////////////////////////////*/

  string public name;

  string public symbol;

  uint8 public immutable decimals;

  /*//////////////////////////////////////////////////////////////
                            ERC20 STORAGE
  //////////////////////////////////////////////////////////////*/

  uint256 public totalSupply;

  mapping(address => uint256) public balanceOf;

  mapping(address => mapping(address => uint256)) public allowance;

  /*//////////////////////////////////////////////////////////////
                          EIP-2612 STORAGE
  //////////////////////////////////////////////////////////////*/

  uint256 internal immutable INITIAL_CHAIN_ID;

  bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;

  mapping(address => uint256) public nonces;

  /*//////////////////////////////////////////////////////////////
                             CONSTRUCTOR
  //////////////////////////////////////////////////////////////*/

  constructor(
    string memory _name,
    string memory _symbol,
    uint8 _decimals
  ) {
    name = _name;
    symbol = _symbol;
    decimals = _decimals;

    INITIAL_CHAIN_ID = block.chainid;
    INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
  }

  /*//////////////////////////////////////////////////////////////
                             ERC20 LOGIC
  //////////////////////////////////////////////////////////////*/

  function approve(address spender, uint256 amount)
  public
  virtual
  returns (bool)
  {
    allowance[msg.sender][spender] = amount;

    emit Approval(msg.sender, spender, amount);

    return true;
  }

  function transfer(address to, uint256 amount)
  public
  virtual
  returns (bool)
  {
    balanceOf[msg.sender] -= amount;

    // Cannot overflow because the sum of all user
    // balances can't exceed the max uint256 value.
    unchecked {
      balanceOf[to] += amount;
    }

    emit Transfer(msg.sender, to, amount);

    return true;
  }

  function transferFrom(
    address from,
    address to,
    uint256 amount
  ) public virtual returns (bool) {
    uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.

    if (allowed != type(uint256).max)
      allowance[from][msg.sender] = allowed - amount;

    balanceOf[from] -= amount;

    // Cannot overflow because the sum of all user
    // balances can't exceed the max uint256 value.
    unchecked {
      balanceOf[to] += amount;
    }

    emit Transfer(from, to, amount);

    return true;
  }

  /*//////////////////////////////////////////////////////////////
                           EIP-2612 LOGIC
  //////////////////////////////////////////////////////////////*/

  function permit(
    address owner,
    address spender,
    uint256 value,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
  ) public virtual;

  function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
    return
      block.chainid == INITIAL_CHAIN_ID
        ? INITIAL_DOMAIN_SEPARATOR
        : computeDomainSeparator();
  }

  function computeDomainSeparator() internal view virtual returns (bytes32) {
    return
      keccak256(
      abi.encode(
        keccak256(
          "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
        ),
        keccak256(bytes(name)),
        keccak256("1"),
        block.chainid,
        address(this)
      )
    );
  }

  /*//////////////////////////////////////////////////////////////
                      INTERNAL MINT/BURN LOGIC
  //////////////////////////////////////////////////////////////*/

  function _mint(address to, uint256 amount) internal virtual {
    totalSupply += amount;

    // Cannot overflow because the sum of all user
    // balances can't exceed the max uint256 value.
    unchecked {
      balanceOf[to] += amount;
    }

    emit Transfer(address(0), to, amount);
  }

  function _burn(address from, uint256 amount) internal virtual {
    balanceOf[from] -= amount;

    // Cannot underflow because a user's balance
    // will never be larger than the total supply.
    unchecked {
      totalSupply -= amount;
    }

    emit Transfer(from, address(0), amount);
  }
}

contract myToken is ERC20 {
  constructor(
    string memory _name,
    string memory _symbol,
    uint8 _decimals
  ) ERC20(_name, _symbol, _decimals) {}

  function permit(
    address owner,
    address spender,
    uint256 value,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
  ) public virtual override {
    require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");

    // Unchecked because the only math done is incrementing
    // the owner's nonce which cannot realistically overflow.
    unchecked {
      address recoveredAddress = ecrecover(
        keccak256(
          abi.encodePacked(
            "\x19\x01",
            DOMAIN_SEPARATOR(),
            keccak256(
              abi.encode(
                keccak256(
                  "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
                ),
                owner,
                spender,
                value,
                nonces[owner]++,
                deadline
              )
            )
          )
        ),
        v,
        r,
        s
      );

      require(
        recoveredAddress != address(0) && recoveredAddress == owner,
        "INVALID_SIGNER"
      );

      allowance[recoveredAddress][spender] = value;
    }

    emit Approval(owner, spender, value);
  }
}

继承合约

继承合约时,也需要初始化合约的构造函数

合约函数的继承

  • 按照继承顺序,继承父合约的全部状态变量
    • immutable 变量需要在 构造函数中声明
    • constant 变量直接继承使用
    • 按照继承顺序,继承父合约的全部修饰器
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

contract parent {
    uint256 public immutable base;
    uint256 public constant DAY = 1 days;
    uint256 public num;

    constructor(uint256 _base) {
        base = _base;
    }

    modifier isOdd(uint256 number) {
        require(number % 2 == 0);
        _;
    }

    function checkNumber(uint256 number) external isOdd(number) {
        //do someThing
    }
}

contract a is parent {
    constructor(uint256 _base) parent(_base) {
        base = _base;
    }

    function doAnotherCheck(uint256 number) external isOdd(number) {
        // num in slot0
        // immutable|constant 直接编码到合约codes,不占slot
        num += number;
    }
}
  • 按照继承顺序,继承父合约的全部函数
    • override 可以重写函数
    • super 调用父合约的函数,补充增加函数的限制条件
  • 后继承的函数会重写之前继承的函数,因此不能菱形继承(函数互相重写依赖)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/* Graph of inheritance
    A
   / \
  B   C
   \ 
    D,

*/

contract A {
  function foo() public pure virtual returns (string memory) {
    return "A";
  }
}

// Contracts inherit other contracts by using the keyword 'is'.
contract B is A {
  // Override A.foo()
  function foo() public pure virtual override returns (string memory) {
    return "B";
  }
}

contract C is A {
  // Override A.foo()
  function foo() public pure virtual override returns (string memory) {
    return "C";
  }
}

// Contracts can inherit from multiple parent contracts.
// When a function is called that is defined multiple times in
// different contracts, parent contracts are searched from
// right to left, and in depth-first manner.

contract D is B, C {
  // D.foo() returns "C"
  // since C is the right most parent contract with function foo()
  function foo() public pure override(B, C) returns (string memory) {
    return super.foo();// C ->foo() ==>return C
  }
}
  • 父合约中 private 修饰的状态变量无法直接读取或更新,但可以使用 sload|sstore 直接操作 slot
  • 父合约中 private 修饰的函数无法调用
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

contract parent {
    uint256 public immutable base;
    uint256 public num; //slot0
    uint256 private prinum; //slot1
    uint256 public num2; // slot2

    constructor(uint256 _base) {
        base = _base;
    }

    modifier isOdd(uint256 number) {
        require(number % 2 == 0);
        _;
    }

    function checkNumber(uint256 number) external isOdd(number) {
        //do someThing
    }
}

contract a is parent {
    constructor(uint256 _base) parent(_base) {
        base = _base;
    }

    function getSlotValue(uint256 slot) public view returns (bytes32 value) {
        assembly {
            value := sload(slot)
        }
    }

    function sstore_x(uint256 slot, uint256 newval) public {
        assembly {
            sstore(slot, newval)
        }
    }
}

代理合约

  • 代理合约底层采用 delegateCall,分离逻辑和数据。
  • 由于逻辑和数据分离,后续可以灵活替换逻辑业务
  • 代理合约按照逻辑合约代码去更新/读取 EVM 环境中的 slot 的状态变量
    • 因此替换逻辑合约时,应该保持 slot 的状态变量的顺序
    • 新增参数只能在末端添加

代理合约逻辑

  • 用户直接对接代理合约,代理合约存储合约数据
    • 代理合约 delegateCall 调用逻辑合约,只能更改代理合约内部的状态变量
  • 逻辑合约仅仅负责代理合约数据处理的逻辑
    • 逻辑合约的数据不受代理合约的影响
  • 因此,需要独立初始化代理合约和逻辑合约的内部参数

代理合约插槽存储

This is the keccak-256 hash of “eip1967.proxy.implementation” subtracted by 1.

bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

This is the keccak-256 hash of “eip1967.proxy.admin” subtracted by 1.

bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

最简代理合约(不可升级clone)

最小代理基于EIP1167,由三部分组成:

  • initcode,拷贝 calldata 数据
  • 指定逻辑合约地址
  • 执行 delegateCall,返回执行结果或者失败 revert

examples

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/utils/Create2.sol";

contract Factory {
    error AccountCreationFailed();
    error InvalidSignerInput();
    error InvalidOwnerInput();
    error DuplicatedAddress();
    event AccountCreated(address indexed _account);

    address[] public userwallet;
    address public implementation;
    address public manager;
    address public owner;
    mapping(string => address) identiWallet;

    function getWalletLength() public view returns (uint256) {
        return userwallet.length;
    }

    constructor(address _manager, address _implement) payable {
        owner = msg.sender;
        manager = _manager;
        implementation = _implement;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "NOT_OWNER");
        _;
    }

    modifier onlyManager() {
        require(msg.sender == manager, "NOT_OWNER");
        _;
    }

    function updateOwner(address newOwner) external onlyOwner {
        require(
            newOwner != address(0) && newOwner != owner,
            "DUP/INVALID_NEWOWNER"
        );
        owner = newOwner;
    }

    function updateManager(address newManager) external onlyOwner {
        require(
            newManager != address(0) && newManager != manager,
            "DUP/INVALID_NEWMANAGER"
        );
        manager = newManager;
    }

    function updateImpl(address newImpl) external onlyOwner {
        require(
            newImpl != address(0) && newImpl != implementation,
            "DUP/INVALID_NEWIMPL"
        );
        implementation = newImpl;
    }

    function getCreationCode(bytes32 _identifier)
    internal
    view
    returns (bytes memory)
    {
        return
            abi.encodePacked(
            hex"3d60ad80600a3d3981f3363d3d373d3d3d363d73",
            implementation,
            hex"5af43d82803e903d91602b57fd5bf3",
            abi.encode(_identifier)
        );
    }

    function createAccount(
        address _owner,
        address _signer,
        string memory _identifier
    ) external onlyManager returns (address _account) {
        (bytes memory code, bytes memory salt) = checkAccount(
            _owner,
            _signer,
            _identifier
        );

        assembly {
            _account := create2(0, add(code, 0x20), mload(code), salt)
        }
        if (_account == address(0)) revert AccountCreationFailed();
        emit AccountCreated(_account);
        (bool success, bytes memory result) = _account.call(
            abi.encodeWithSignature(
                "initData(address,address,address)",
                _owner,
                manager,
                _signer
            )
        );
        if (!success) {
            assembly {
                revert(add(result, 32), mload(result))
            }
        }
        identiWallet[_identifier] = _account;
        userwallet.push(_account);
    }

    function checkAccount(
        address _owner,
        address _signer,
        string memory _identifier
    ) internal view returns (bytes memory code, bytes memory salt) {
        if (_owner == address(0)) revert InvalidOwnerInput();
        if (_signer == address(0)) revert InvalidSignerInput();
        require(
            identiWallet[_identifier] == address(0),
            "ONLY_ALLOW_ONE_WALLET"
        );
        code = getCreationCode(convertStringToByte32(_identifier));
        salt = abi.encode(_identifier, _owner, _signer);
        address _account = Create2.computeAddress(
            bytes32(salt),
            keccak256(code)
        );
        if (_account.code.length != 0) revert DuplicatedAddress();
    }

    function account(
        address _owner,
        address _signer,
        string memory _identifier
    ) external view returns (address _account) {
        bytes memory code = getCreationCode(convertStringToByte32(_identifier));
        bytes memory salt = abi.encode(_identifier, _owner, _signer);
        _account = Create2.computeAddress(bytes32(salt), keccak256(code));
        return _account;
    }

    function convertStringToByte32(string memory _texte)
    internal
    pure
    returns (bytes32 result)
    {
        assembly {
            result := mload(add(_texte, 32))
        }
    }
}

简单可升级代理合约实现

逻辑合约地址

  • eip1967 允许函数更新 IMPLEMENTATION_SLOT 数据 (严格的权限控制),

initialize

  • 一般情况下,逻辑合约定义 initialize() 函数,按照插槽顺序初始化代理合约数据
    • 但是需要注意: initialize() 应该只能调用一次,或者拥有严格的权限控制
    • 代理合约的初始化不会影响逻辑合约的状态变量
    • 逻辑合约必须单独执行数据的初始化,并确保逻辑合约中的初始化只能调用一次,或者拥有严格的权限控制
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "erc721a/contracts/extensions/ERC721ABurnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract LogisNFT is Ownable, ERC721ABurnable {
    bool public mintable;
    uint256 public maxSupply;
    uint256 private initilaized;
    address private launchcaller;
    // Current base URI.
    string public currentBaseURI;
    // The suffix for the token URL, e.g. ".json".
    string private tokenURISuffix;
    string private nftname;
    string private nftsymbol;
    error NotMintable();

    constructor() payable ERC721A(nftname, nftsymbol) Ownable(_msgSender()) {
        initilaized = 1;
    }

    function initailize(
        string memory _name,
        string memory _symbol,
        string memory _tokenURISuffix,
        string memory _currentBaseURI,
        uint256 _maxSupply,
        address _caller
    ) public virtual {
        require((initilaized == 0), "ALREADY_INITIALIZED");
        nftname = _name;
        nftsymbol = _symbol;
        tokenURISuffix = _tokenURISuffix;
        currentBaseURI = _currentBaseURI;
        maxSupply = _maxSupply;
        launchcaller = _caller;
        initilaized = 1;
    }

    /**
     * @dev Returns the token collection name.
     */
    function name()
        public
        view
        virtual
        override(ERC721A, IERC721A)
        returns (string memory)
    {
        return nftname;
    }

    /**
     * @dev Returns the token collection symbol.
     */
    function symbol()
        public
        view
        virtual
        override(ERC721A, IERC721A)
        returns (string memory)
    {
        return nftsymbol;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function caller() public view virtual returns (address) {
        return launchcaller;
    }

    /**
     * @dev Sets mintable.
     */
    function setMintable(bool _mintable) external onlyOwner {
        mintable = _mintable;
    }
}

contract ProxyBase is ERC1967Proxy, Ownable {
    constructor(
        address implementation,
        bytes memory _data,
        address _owner
    ) ERC1967Proxy(implementation, _data) Ownable(_owner) {}

    function implement() external view returns (address) {
        return _implementation();
    }

    function upgradeImpl(address implementation, bytes memory _data)
        public
        virtual
        onlyOwner
    {
        ERC1967Utils.upgradeToAndCall(implementation, _data);
    }

    function renounceOwnership() public view override onlyOwner {
        revert("CLOSED_INTERFACE");
    }

    receive() external payable {}
}

contract Factory is Ownable {
    address[] public proxys;
    event ProxyCreated(address indexed _account);

    constructor() Ownable(_msgSender()) {}

    function newproxy(
        address implementation,
        string memory _name,
        string memory _symbol,
        string memory _tokenURISuffix,
        string memory _currentBaseURI,
        uint256 _maxSupply,
        address _caller,
        address _owner
    ) external onlyOwner {
        bytes memory _data = abi.encodeWithSignature(
            "initailize(string,string,string,string,uint256,address)",
            _name,
            _symbol,
            _tokenURISuffix,
            _currentBaseURI,
            _maxSupply,
            _caller
        );
        ProxyBase proxy = new ProxyBase(implementation, _data, _owner);
        proxys.push(address(proxy));
        emit ProxyCreated(address(proxy));
    }

    function renounceOwnership() public view override onlyOwner {
        revert("CLOSED_INTERFACE");
    }
}

合约升级

  • 代理合约直接 delegateCall 逻辑合约
  • 按照逻辑合约的代码更新 代理合约 EVM slot 数据

因此,合于升级后不能影响旧的状态变量相对顺序

  • 合约升级只能添加新的函数、事件、error、结构体、immutable|constant 变量
  • 合约升级允许更新函数逻辑
  • 合约升级可以在最后添加新的状态变量,确保旧状态变量的顺序
  • 合约升级不能更改状态变量顺序、不能继承新的合约(会改变状态变量顺序)

beacon代理合约

  1. 多个代理合约使用同一个逻辑合约,并且通过单笔交易可以升级多个代理合约的逻辑合约地址
  2. 适用于一个逻辑产生多个衍生合约的场景(班级学生采用同一个逻辑管理,但是每人拥有各自的状态)

beacon合约

  • beacon 合约作为灯塔,提供当前 implementation 地址
  • 全部代理合约去 beacon 合约读取逻辑合约地址,发送 delegateCall 交易

  • 更换新的逻辑合约后,只需要更新 beacon 合约的 IMPLEMENTATION_SLOT 数据,就可以实现 all proxy 的逻辑地址更新

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "erc721a/contracts/extensions/ERC721ABurnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract LogisNFT is Ownable, ERC721ABurnable {
    bool public mintable;
    uint256 public maxSupply;
    uint256 private initilaized;
    address private launchcaller;
    // Current base URI.
    string public currentBaseURI;
    // The suffix for the token URL, e.g. ".json".
    string private tokenURISuffix;
    string private nftname;
    string private nftsymbol;
    error NotMintable();

    constructor() payable ERC721A(nftname, nftsymbol) Ownable(_msgSender()) {
        initilaized = 1;
    }

    function initailize(
        string memory _name,
        string memory _symbol,
        string memory _tokenURISuffix,
        string memory _currentBaseURI,
        uint256 _maxSupply,
        address _caller
    ) public virtual {
        require((initilaized == 0), "ALREADY_INITIALIZED");
        nftname = _name;
        nftsymbol = _symbol;
        tokenURISuffix = _tokenURISuffix;
        currentBaseURI = _currentBaseURI;
        maxSupply = _maxSupply;
        launchcaller = _caller;
        initilaized = 1;
    }

    /**
     * @dev Returns the token collection name.
     */
    function name()
    public
    view
    virtual
    override(ERC721A, IERC721A)
    returns (string memory)
    {
        return nftname;
    }

    /**
     * @dev Returns the token collection symbol.
     */
    function symbol()
    public
    view
    virtual
    override(ERC721A, IERC721A)
    returns (string memory)
    {
        return nftsymbol;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function caller() public view virtual returns (address) {
        return launchcaller;
    }

    /**
     * @dev Sets mintable.
     */
    function setMintable(bool _mintable) external onlyOwner {
        mintable = _mintable;
    }
}

// 管理Implementation
contract Beacon is UpgradeableBeacon {
    constructor(address implementation_)
    UpgradeableBeacon(implementation_, _msgSender())
    {}
}

// 管理beacon,用户直接交互的合约
// address private immutable _beacon;
// beacon地址不允许更新,值直接写到合约codes,读取的一直是合约codes中初始化的那个值
// Factory产生的子合约
contract ProxyBase is BeaconProxy, Ownable {
    constructor(
        address beacon,
        bytes memory data, // delegateCall implementation()'s address
        address _owner
    ) BeaconProxy(beacon, data) Ownable(_owner) {}

    function implement() external view returns (address) {
        return _implementation();
    }

    function renounceOwnership() public view override onlyOwner {
        revert("CLOSED_INTERFACE");
    }

    function getBeacon() external view returns (address) {
        return _getBeacon();
    }

    receive() external payable {}
}

contract Factory is Ownable {
    address[] public proxys;
    event ProxyCreated(address indexed _account);

    constructor() Ownable(_msgSender()) {}

    function newproxy(
        address beacon,
        string memory _name,
        string memory _symbol,
        string memory _tokenURISuffix,
        string memory _currentBaseURI,
        uint256 _maxSupply,
        address _caller,
        address _owner
    ) external onlyOwner {
        bytes memory _data = abi.encodeWithSignature(
            "initailize(string,string,string,string,uint256,address)",
            _name,
            _symbol,
            _tokenURISuffix,
            _currentBaseURI,
            _maxSupply,
            _caller
        );
        ProxyBase proxy = new ProxyBase(beacon, _data, _owner);
        proxys.push(address(proxy));
        emit ProxyCreated(address(proxy));
    }

    function renounceOwnership() public view override onlyOwner {
        revert("CLOSED_INTERFACE");
    }
}

合约调用

Call

  • call 的外部调用在 low-level 层面新启一个 EVM 作为外部合约被 call 后的交易的执行环境
  • 在新启的 EVM 执行环境中,整体复制外部合约的代码
    • 按照外部合约的逻辑执行当前交易,更新外部合约的状态变量
  • 对于外部合约来讲,当前被 call 的交易处在新的 EVM 执行环境,交易的发起方就是发起调用的合约地址

High-level

contract interface

  • 通过函数接口调用外部合约函数,_Name(_Address).func()
  • call 返回 (bool success, bytes memory data)
    • 通过函数直接调用的话,Solidity 在语言层面直接判断返回值
    • 返回值如果为 false ,抛出 revert() 异常

Low-level

function selector

  • 外部函数支持通过合约函数选择器传参调用, abi.encodeWithSignature | encodeWithSelector | encodePacked
    • public|external 修饰的函数允许外部调用 以及 继承的合约使用
    • internal|private 修饰的函数不支持外部调用
      • internal 修饰的函数允许继承使用
      • private 修饰的函数不允许继承使用
  • 外部调用返回 (bool success, bytes memory data)
    • boolean 表明当前调用是否成功
    • data 是执行函数返回的数据
    • call 执行失败的话,不会 revert 回滚交易,因此需要执行异常判断
  • call 外部合约不存在的函数
    • 外部合约存在缺省 fallback() ,就执行 fallback() 逻辑
    • 不存在缺省函数的话,直接返回 false

Call types validation

  • call
    • 交易执行在新的 EVM 执行环境
    • address(this) = 被调用的外部合约的地址

Solidity Examples

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

contract Caller {
  // first call to ops()
  // just return false, but not revert
  function callByCall(address _address) public returns (bool) {
    (bool success, bytes memory returndata) = _address.call(
      abi.encodeWithSignature("ops()")
    );
    if (!success) _revert(returndata, "Call_Faliure");
    return success;
  }

  // second call to ops()
  // it will revert()
  function callByInterface(address _address) public {
    Called called = Called(_address);
    called.ops();
  }

  function _revert(bytes memory returndata, string memory errorMessage)
  private
  pure
  {
    // Look for revert reason and bubble it up if present
    if (returndata.length > 0) {
      // The easiest way to bubble the revert reason is using memory via assembly
      /// @solidity memory-safe-assembly
      assembly {
        let returndata_size := mload(returndata)
        revert(add(32, returndata), returndata_size)
      }
    } else {
      revert(errorMessage);
    }
  }
}

contract Called {
  // ops() always reverts
  function ops() public {
    revert();
  }
}

Calling address(0)

High-level

  • high-levelsolidity 层面直接发起外部合约调用
    • 在发起对地址的调用前先校验 caller 是否合法
      • 只能对合约代码不为空的地址发起外部调用

Lower-level

  • lower-levelEVM 层面直接发起外部合约调用
    • 直接发起对地址的调用,不会在语言层面校验 caller 是否合法

外部调用 Error:

  • 执行中遇到 REVERT 关键字
  • out-of-gas
  • 异常(/0,out-of-bound)

Solidity Contracts

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

interface ERC20 {
  function transferFrom(
    address from,
    address to,
    uint256 amount
  ) external returns (bool);
}

contract receiver {
  constructor() {}

  event ReceiveReceived(address Sender, uint256 Value);
  event FallbackReceived(address Sender, uint256 Value);

  receive() external payable {
    emit ReceiveReceived(msg.sender, msg.value);
  }

  fallback() external payable {
    emit FallbackReceived(msg.sender, msg.value);
  }
}

contract payer {
  constructor() payable {}

  function sendValue(address payable recipient, uint256 amount) external {
    bool success = recipient.send(amount); //ReceiveReceived
    if (!success) {
      revert("Send Trans Failure");
    }
  }

  function transferValue(address payable recipient, uint256 amount) external {
    recipient.transfer(amount); //ReceiveReceived
  }

  function callSendValue(
    address payable recipient,
    uint256 amount,
    bytes memory _data
  ) external {
    (bool success, ) = recipient.call{value: amount}(_data); //ReceiveReceived
    if (!success) {
      revert("call_send_value_failure");
    }
  }

  // 11053 gas cost
  function safeTransferNativeToken(
    address to,
    uint256 amount,
    uint256 txGas
  ) external {
    bool success;
    /// @solidity memory-safe-assembly
    assembly {
    // Transfer the ETH and store if it succeeded or not.
    //success := call(gas(), to, amount, 0, 0, 0, 0)
      success := call(txGas, to, amount, 0, 0, 0, 0)
    }
    require(success, "ETH_TRANSFER_FAILED");
  }

  function safeTransfer(
    ERC20 token,
    address to,
    uint256 amount
  ) internal {
    bool success;

    /// @solidity memory-safe-assembly
    assembly {
    // Get a pointer to some free memory.
      let freeMemoryPointer := mload(0x40)

    // Write the abi-encoded calldata into memory, beginning with the function selector.
      mstore(
        freeMemoryPointer,
        0xa9059cbb00000000000000000000000000000000000000000000000000000000
      )
      mstore(
        add(freeMemoryPointer, 4),
        and(to, 0xffffffffffffffffffffffffffffffffffffffff)
      ) // Append and mask the "to" argument.
      mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.

      success := and(
      // Set success to whether the call reverted, if not we check it either
      // returned exactly 1 (can't just be non-zero data), or had no return data.
        or(
          and(eq(mload(0), 1), gt(returndatasize(), 31)),
          iszero(returndatasize())
        ),
      // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
      // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
      // Counterintuitively, this call must be positioned second to the or() call in the
      // surrounding and() call or else returndatasize() will be zero during the computation.
        call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
      )
    }

    require(success, "TRANSFER_FAILED");
  }
}

合约调用

delegateCall

  • delegateCalllow-level 层将外部合约的代码整个复制到当前 EVM 环境,与 origin 交易复用同一个 EVM 执行环境
    • delegateCall 执行在本合约的 EVM 环境
    • delegateCall 拥有完整的外部调用合约的合约代码
    • delegateCall 按照外部合约代码去更新/读取 EVM 环境中的 slot 的状态变量

返回状态

  • delegateCall 返回 (bool success, bytes memory data)
    • boolean 表明当前调用是否成功
    • data 是执行函数返回的数据
    • delegateCall 执行失败的话,不会 revert 回滚交易,因此需要执行异常判断
  • delegateCall 外部合约不存在的函数
    • 外部合约存在缺省 fallback() ,就执行 fallback() 逻辑
    • 不存在缺省函数的话,直接返回 false

Solidity Examples

  • delegateCall 按照外部合约代码去更新/读取 EVM 环境中的 slot 的状态变量
  • 示例合约在 EVM 中的逻辑为: slot1 += slot0
    • 本合约按照上述逻辑更新自己合约内部的 slot 参数
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

contract Called {
  uint256 base = 3;
  uint256 public number; //slot 1

  function increment() public returns (uint256) {
    number += base; // slot1  += slot0
    return number;
  }
}

contract Caller {
  uint256 base = 99; // 按照业务逻辑会读取slot0数据,参与计算
  uint256 public myNumber; // 每次调用:slot1 += slot0(myNumber+=99)
  // there is a new storage variable here
  address public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;

  function execute(address to, uint256 txGas)
  external
  returns (bool success)
  {
    bytes memory data = abi.encodeWithSignature("increment()");
    return _execute(to, data, txGas);
  }

  function _execute(
    address to,
    bytes memory data,
    uint256 txGas
  ) internal returns (bool success) {
    assembly {
      success := delegatecall(
        txGas,
        to,
        add(data, 0x20),
        mload(data),
        0,
        0
      )
    }
  }

  function delegateCallIncrement(
    address delegatedCalled //28187 gas cost
  ) public returns (uint256) {
    (bool success, bytes memory resdata) = delegatedCalled.delegatecall(
      abi.encodeWithSignature("increment()") //0xd09de08a
    );
    if (!success) {
      assembly {
        revert(add(resdata, 32), mload(resdata))
      }
    } else {
      // 解码call|delegateCall的返回值
      return abi.decode(resdata, (uint256));
    }
  }
}

Immutable|Constant in delegateCall

  • delegateCall 是将外部合约的代码整个拷贝到当前 EVM 执行
  • immutable|constant 是编码到合约代码存储,并不占据 slot 存储
  • 也就是 immutable|constant 类型的数据,是从外部合约代码直接读取
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Caller {
    uint256 private immutable a = 3;

    function getValueDelegate(address called) public returns (uint256) {
        (bool success, bytes memory data) = address(called).delegatecall(
            abi.encodeWithSignature("getValue()")
        );
        if (!success) {
            _revert(data, "Calles_Failure");
        }
        return abi.decode(data, (uint256)); // returns 2
    }

    function _revert(bytes memory returndata, string memory errorMessage)
        private
        pure
    {
        // Look for revert reason and bubble it up if present
        if (returndata.length > 0) {
            // The easiest way to bubble the revert reason is using memory via assembly
            /// @solidity memory-safe-assembly
            assembly {
                let returndata_size := mload(returndata)
                revert(add(32, returndata), returndata_size)
            }
        } else {
            revert(errorMessage);
        }
    }
}

contract Called {
    uint256 private immutable a = 2;

    function getValue() public pure returns (uint256) {
        return a;
    }
}

Call types validation

  • delegateCall
    • 交易执行在当前的 EVM 执行环境
    • address(this) = 当前合约的地址
    • __selfimmutable 类型数据,因此该值不从 slot 读取,而是从外部合约代码读取:读取到的是外部合约的合约地址
    • 因此,采用 delegateCall 的话,两者的地址应该不同
  • call
    • 交易执行在新的 EVM 执行环境
    • address(this) = 被调用的外部合约的地址
    • __selfimmutable 类型数据,因此该值不从 slot 读取,而是从外部合约代码读取:读取到的是外部合约的合约地址
    • 因此,采用 call 的话,两者的地址应该相同

总之:

delegateCall –> require(address(this) != __self)

call –> require(address(this) == __self)

    address private immutable __self = address(this);
    /**
     * @dev Check that the execution is being performed through a delegatecall call
     * 要求使用delegateCall调用该修饰器修饰的函数
     * 使用delegateCall调用时:
     ** 1. address(this) = 当前合约的地址
     ** 2. __self是immutable类型数据,从外部合约代码读取,读取到的应该是 外部合约的合约地址
     * 因此,采用 delegateCall的话,两者的地址应该不同
     */
    modifier onlyProxy() {
        require(
            address(this) != __self,
            "Function must be called through delegatecall"
        );
        require(
            _getImplementation() == __self,
            "Function must be called through active proxy"
        );
        _;
    }

    /**
     * @dev Check that the execution is not being performed through a delegate call.
    * 要求使用call调用该修饰器修饰的函数
     * 使用call调用时, 交易被打包进新的EVM 执行环境
     ** 1. address(this) = 外部调用的合约地址
     ** 2. __self是immutable类型数据,从外部合约代码读取,读取到的应该是 外部合约的合约地址
     * 因此,采用 call的话,两者的地址应该相同
     */
    modifier notDelegated() {
        require(
            address(this) == __self,
            "UUPSUpgradeable: must not be called through delegatecall"
        );
        _;
    }

staticCall

  1. staticcall{gas: gasAmount}(abiEncodedArguments);
  2. staticcall 外部合约函数只读数据
    1. 函数必须被 view|pure 修饰,表明只读
  3. 天然适用于 预编译合约
  4. staticcall 无法更新状态变量:
    1. 更新合约内部的状态变量
    2. emit event 触发链上事件
    3. 创建其他合约
    4. self destruct 销毁合约(合约余额强制转移到传参地址)
    5. 转账,更新账户余额
    6. 调用其他未标记 pure/view 的函数
    7. 使用内联编码更改状态数据库

Solidity Examples

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

interface IERC20 {
    function balanceOf(address) external view returns (uint256);
}

contract Token is IERC20 {
    mapping(address => uint256) balance;

    function mint(address addr, uint256 qty) external {
        balance[addr] += qty;
    }

    function balanceOf(address addr) external view returns (uint256) {
        return balance[addr];
    }
}

contract ERC20User {
    // 5837 gas cost
    function myBalance(IERC20 token, address addr)
        public
        view
        returns (uint256 balance)
    {
        balance = token.balanceOf(addr);
    }

    // 6294 gas cost
    function myBalanceLowLevelEquivalent(IERC20 token, address addr)
        public
        view
        returns (uint256 balance)
    {
        (bool ok, bytes memory result) = address(token).staticcall(
            abi.encodeWithSignature("balanceOf(address)", addr)
        );
        require(ok);

        balance = abi.decode(result, (uint256));
    }
}

Security Issues

Denial of Service

staticCall 支持指定 gas 63/64 执行只读的调用,但是对方函数逻辑不明,存在恶意消耗 gas 的安全隐患

Read only Re-entrancy

只读函数会受到其他函数的影响

Preference

https://www.rareskills.io/post/solidity-staticcall

预编译合约(0x01 to 0x09)

  1. Elliptic curve digital signature recovery
  • 0x01: ecRecover
    • 签名验证失败会返回 address(0) ,不会 revert 整笔交易
    • 需要进行签名地址校验 ECDSA校验签名
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/ECDSA.sol)
pragma solidity ^0.8.20;

/**
 * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations.
 *
 * These functions can be used to verify that a message was signed by the holder
 * of the private keys of a given address.
 */
library ECDSA {
    enum RecoverError {
        NoError,
        InvalidSignature,
        InvalidSignatureLength,
        InvalidSignatureS
    }

    /**
     * @dev The signature derives the `address(0)`.
     */
    error ECDSAInvalidSignature();

    /**
     * @dev The signature has an invalid length.
     */
    error ECDSAInvalidSignatureLength(uint256 length);

    /**
     * @dev The signature has an S value that is in the upper half order.
     */
    error ECDSAInvalidSignatureS(bytes32 s);

    /**
     * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not
     * return address(0) without also returning an error description. Errors are documented using an enum (error type)
     * and a bytes32 providing additional information about the error.
     *
     * If no error is returned, then the address can be used for verification purposes.
     *
     * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
     * this function rejects them by requiring the `s` value to be in the lower
     * half order, and the `v` value to be either 27 or 28.
     *
     * IMPORTANT: `hash` _must_ be the result of a hash operation for the
     * verification to be secure: it is possible to craft signatures that
     * recover to arbitrary addresses for non-hashed data. A safe way to ensure
     * this is by receiving a hash of the original message (which may otherwise
     * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
     *
     * Documentation for signature generation:
     * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js]
     * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers]
     */
    function tryRecover(
        bytes32 hash,
        bytes memory signature
    ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
        if (signature.length == 65) {
            bytes32 r;
            bytes32 s;
            uint8 v;
            // ecrecover takes the signature parameters, and the only way to get them
            // currently is to use assembly.
            assembly ("memory-safe") {
                r := mload(add(signature, 0x20))
                s := mload(add(signature, 0x40))
                v := byte(0, mload(add(signature, 0x60)))
            }
            return tryRecover(hash, v, r, s);
        } else {
            return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length));
        }
    }

    /**
     * @dev Returns the address that signed a hashed message (`hash`) with
     * `signature`. This address can then be used for verification purposes.
     *
     * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
     * this function rejects them by requiring the `s` value to be in the lower
     * half order, and the `v` value to be either 27 or 28.
     *
     * IMPORTANT: `hash` _must_ be the result of a hash operation for the
     * verification to be secure: it is possible to craft signatures that
     * recover to arbitrary addresses for non-hashed data. A safe way to ensure
     * this is by receiving a hash of the original message (which may otherwise
     * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
     */
    function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately.
     *
     * See https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signatures]
     */
    function tryRecover(
        bytes32 hash,
        bytes32 r,
        bytes32 vs
    ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
        unchecked {
            bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
            // We do not check for an overflow here since the shift operation results in 0 or 1.
            uint8 v = uint8((uint256(vs) >> 255) + 27);
            return tryRecover(hash, v, r, s);
        }
    }

    /**
     * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately.
     */
    function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Overload of {ECDSA-tryRecover} that receives the `v`,
     * `r` and `s` signature fields separately.
     */
    function tryRecover(
        bytes32 hash,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
        // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
        // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
        // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
        // signatures from current libraries generate a unique signature with an s-value in the lower half order.
        //
        // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
        // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
        // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
        // these malleable signatures as well.
        if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
            return (address(0), RecoverError.InvalidSignatureS, s);
        }

        // If the signature is valid (and not malleable), return the signer address
        address signer = ecrecover(hash, v, r, s);
        if (signer == address(0)) {
            return (address(0), RecoverError.InvalidSignature, bytes32(0));
        }

        return (signer, RecoverError.NoError, bytes32(0));
    }

    /**
     * @dev Overload of {ECDSA-recover} that receives the `v`,
     * `r` and `s` signature fields separately.
     */
    function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided.
     */
    function _throwError(RecoverError error, bytes32 errorArg) private pure {
        if (error == RecoverError.NoError) {
            return; // no error: do nothing
        } else if (error == RecoverError.InvalidSignature) {
            revert ECDSAInvalidSignature();
        } else if (error == RecoverError.InvalidSignatureLength) {
            revert ECDSAInvalidSignatureLength(uint256(errorArg));
        } else if (error == RecoverError.InvalidSignatureS) {
            revert ECDSAInvalidSignatureS(errorArg);
        }
    }
}
  1. Hash methods to interact with bitcoin and zcash
  • 0x02 and 0x03: SHA-256 and RIPEMD-160
    • Ethereum 使用 keccak256 作为地址的哈希算法
    • Bitcoin 使用 SHA-256 and RIPEMD-160 作为基础的哈希计算
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Called {
  function hashSha256(uint256 numberToHash) public view returns (bytes32 h) {
    (bool ok, bytes memory out) = address(2).staticcall(
      abi.encode(numberToHash)
    );
    require(ok);
    h = abi.decode(out, (bytes32));
  }

  function hashSha256Yul(uint256 numberToHash) public view returns (bytes32) {
    assembly {
      mstore(0, numberToHash) // store number in the zeroth memory word

      let ok := staticcall(gas(), 2, 0, 32, 0, 32)
      if iszero(ok) {
        revert(0, 0)
      }
      return(0, 32)
    }
  }

  function hashRIPEMD160(bytes calldata data)
  public
  view
  returns (bytes20 h)
  {
    (bool ok, bytes memory out) = address(3).staticcall(data);
    require(ok);
    h = bytes20(abi.decode(out, (bytes32)) << 96);
  }
}

Goland Sha256:

	s := hexutil.EncodeBig(big.NewInt(12)) 
	prefix := ""
	num := 64 - len(s[2:])
	for index := 0; index < num; index++ {
		prefix += "0"

	}
	s = s[:2] + prefix + s[2:]
	//fmt.Println(s)
	byteD, err := hexutil.Decode(s)
	if err != nil {
		fmt.Println(err)
	}
	h := sha256.New()
	h.Write(byteD)
	bs := h.Sum(nil)
	fmt.Println(hexutil.Encode(bs))
  1. Memory copying
  • Address 0x04: Identity
    • 拷贝内存数据
  1. Methods to enable elliptic curve math for zero knowledge proofs
  • Address 0x05: Modexp
    • 幂模运算
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract MoxExp {
    function modExp(
        uint256 base,
        uint256 exp,
        uint256 mod
    ) public view returns (uint256) {
        bytes memory precompileData = abi.encode(32, 32, 32, base, exp, mod);
        (bool ok, bytes memory data) = address(5).staticcall(precompileData);
        require(ok, "expMod failed");
        return abi.decode(data, (uint256));
    }

    function Exp(uint256 base, uint256 exp) public view returns (uint256) {
        uint256 max = type(uint256).max;
        bytes memory precompileData = abi.encode(32, 32, 32, base, exp, max);
        (bool ok, bytes memory data) = address(5).staticcall(precompileData);
        require(ok, "expMod failed");
        return abi.decode(data, (uint256));
    }
}
  • Address 0x06 and 0x07 and 0x08: ecAdd, ecMul, and ecPairing (EIP-196 and EIP-197)

Preference

https://www.evm.codes/precompiled?fork=grayGlacier

MerkleProof

构建树

h(a) = h(h(ℓ₁) + h(ℓ₂))
h(e) = h(h(a) + h(b))
h(g) = h(h(e) + h(f))
return root == h(g)

Questions

The attack requires 64 byte leaves

MerkleTree 只运算叶子节点数据的 hash ,上述构造树的过程中:

  • 数据 ℓ₁ 的 叶子节点数据 h(ℓ₁) = 32 bytes
  • a = h(ℓ₁) + h(ℓ₂) = 64 bytes
  1. MerkleTree 校验仅支持 32bytes 的叶子节点数据

校验过程中必须将提供的原值 hash 后参与计算

因此,攻击者无法提供 a = 64 bytes 的原值,proof = [h(b), h(f)] 跳过校验

如果 攻击者提供 h(a) 作为原址,原值再次 hash 后将不满足树的校验

因此,32bytes 的叶子节点可以有效限制 攻击者跳过真实叶子节点的校验攻击问题

  1. MerkleTree 校验如果支持 64bytes 的叶子节点数据

校验过程中必须将 提供的原值 hash 后参与计算

因此,攻击者可以提供 a = 64 bytes 的原值,proof = [h(b), h(f)] 跳过校验

因此,64bytes 的叶子节点不能限制 攻击者跳过真实叶子节点的校验攻击问题,存在安全问题

叶子节点和父节点采用不同的hash运算

  1. 再不限制叶子节点数据的前提下,可以通过采用不同的节点运算方式避免攻击问题
  • 为简化使用,叶子节点执行双 hash h’(x) = h(h(x))
  • 父节点只进行单次 hash 参与构建
    • 此时,攻击者提供 a
    • 根据 hash 运算规则,a 执行 两次 hash 参与校验,此时不满足树的校验

preference

merkle-tree-second-preimage-attack

openzeppelin-merkle-proof

openzeppelin-merkle-proof-golang

contracts

校验签名:

ECDSA(Elliptic Curve Digital Signature Algorithm) 用来对特定的数据进行密码学上的计算,运行数据的签名者(signer)签名封装数据,签名公钥(verifier)可以校验解封数据

  • 签名数据被篡改的话,公钥无法解封出正确的数据
  • 签名公钥和私钥不配对的话,数据无法解封

Signing

  1. Create message to sign
  2. Hash the message
  3. Sign the hash (off chain, keep your private key secret)

Verify

  1. Recreate hash from the original message
  2. Recover signer from signature and hash
  3. Compare recovered signer to claimed signer

preference

ECDSA-trtorial

openzeppelin-ecdsa

contracts

cryptobook

Common mistakes

Div before Mul

Solidity只有整数,不支持小数运算,默认的整数乘除向下取整。

**// Wrong way:**
If principal = 3000,
interest = principal / 3333 * 10000
interest = 3000 / 3333 * 10000
interest = 0 * 10000 (rounding down in division)
interest = 0

// **Correct Calculation:**
If principal = 3000,
interest = principal * 10000 / 3333
interest = 3000 * 10000 / 3333
interest = 30000000 / 3333 interest approx 9000

Fixed point ABDK

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

import "@openzeppelin/contracts/utils/math/Math.sol";
import "https://github.com/abdk-consulting/abdk-libraries-solidity/blob/master/ABDKMath64x64.sol";

contract MathPaper {
    // k, N 都是整数
    function func1(uint256 k, uint256 N) public pure returns (uint64) {
        uint256 numerator = k + N + 1;
        uint256 denominator = N + 1;
        return log2(numerator) - log2(denominator);
    }

    function func3(uint256 dt, uint256 e) public pure returns (uint256) {
        if (dt < e) {
            return 100;
        }
        uint256 exp = Math.ceilDiv((dt - e) * 100, e); //0.2==>20
        uint256 dominator = power(100, exp);
        return 1000000  / dominator;
    }


    function power(uint256 base, uint256 exp) public pure returns (uint256) {
        // Represent base as a fixed-point number.
        int128 baseFixed = ABDKMath64x64.fromUInt(base);

        // Calculate ln(base)
        int128 lnBase = ABDKMath64x64.ln(baseFixed);

        // Represent exp as a fixed-point number.
        int128 expFixed = ABDKMath64x64.divu(exp, 100);

        // Calculate ln(base) * exp
        int128 product = ABDKMath64x64.mul(lnBase, expFixed);

        // Calculate e^(ln(base) * exp)
        int128 result = ABDKMath64x64.exp(product);

        // Multiply by 10^5 to keep 5 decimal places
        result = ABDKMath64x64.mul(result, ABDKMath64x64.fromUInt(10**2));

        // Convert the fixed-point result to a uint and return it.
        return ABDKMath64x64.toUInt(result);
    }

    function log2(uint256 base) public pure returns (uint64) {
        // Represent base as a fixed-point number.
        int128 baseFixed = ABDKMath64x64.fromUInt(base);
        // Calculate ln(base)
        int128 lnBase = ABDKMath64x64.log_2(baseFixed) * 1e4;
        uint64 t = ABDKMath64x64.toUInt(lnBase);
        return t;
    }

}

Not following check-effects-interaction

  1. check-effects-interaction 模式将合约交互放在条件校验、数据更新之后,防止重入攻击。
  2. 合约交互包含 外部合约的 call|delegateCall|staticCall 调用、Native Token 转账
  3. 合约交互操作应该放在整个函数的最后,确保无法正常执行重入攻击(即使重入,数据也是已经更新后的数值)

重入攻击的示例:

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

// DO NOT USE
contract BadBank {
    mapping(address => uint256) public balances;

    constructor() payable {
        require(msg.value == 10 ether, "deposit 10 eth");
    }

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        // should be after the check and effects
        (bool ok, ) = msg.sender.call{value: balances[msg.sender]}("");
        require(ok, "transfer failed");
        // shoule be the first
        balances[msg.sender] = 0;
    }
}

contract attack {
    function deposit(BadBank bank) external payable {
        bank.deposit{value: msg.value}();
        bank.withdraw();
    }

    receive() external payable {
        if (msg.sender.balance >= 1 ether) {
            BadBank(msg.sender).withdraw();
        }
    }
}

Transfer Native Token by ‘transfer’ or ‘send’

  1. transfer or send 转账模式
    1. 为了避免重入攻击,只传递有限的 gas,让调用的合约不能执行过多的合约逻辑
    2. 默认调用时传递 2300 gas,其中 transfer 执行失败会 revertsend 不会 revert 但是会返回待处理的 boolean
  2. sload 字节码在升级后的gas 花销分为: non-warm-2100, warm-100
    1. 升级后如果仍然传递默认的 2300gas,也无法容忍 对方合约中 fallback|receiver 存在读取地址的行为
// in the bank receiver() function, recors the deposit behavior
   receive() external payable {
      balances[msg.sender] += msg.value;
   }
// In the call contract, the deposit transfer function will fail, because only passing 2300 gas could not support the record behavior
contract attack {
   function transfer(BadBank bank) external payable {
      payable(address(bank)).transfer(msg.value);
   }
}
  1. check-effects-interation 模式中使用 call 进行安全转账,处理 call 调用返回的 (bool success, bytes data)
    function sendValue(address payable recipient, uint256 amount) internal {
        if (address(this).balance < amount) {
            revert Errors.InsufficientBalance(address(this).balance, amount);
        }

        (bool success, ) = recipient.call{value: amount}("");
        if (!success) {
            revert Errors.FailedCall();
        }
    }

misusing of tx.origin and msg.sender

  1. tx.origin 指的是当前交易的签名者,初始构造者,在整个交易的生命周期中不会发生变化
  2. msg.sender 指的是当前 EVM 环境中的交易发起方,会随着 EVM 环境的话变化而返回不同的地址

Not using safeTransfer in ERC20

  1. ERC20代币存在两套不同的转账函数
    1. 有些代币的转账函数不存在返回值,比如 USDT的转账
    2. 有些代币的转账函数存在返回值,比如标准的ERC20合约
  2. 针对不同的转账标准, safeTransfer 可以处理全部情况
    1. 在外部调用存在 revert 的情况下,直接 revert
    2. 外部调用不存在返回值的情况下,如果被调用者不是一个合约地址的话,直接 revert报错
    3. 外部调用不存在返回值的情况下,如果被调用者是一个合法合约地址的话,交易成功
    4. 外部调用存在返回值的情况下,如果返回值不是 1(true),直接 revert报错
    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        uint256 returnSize;
        uint256 returnValue;
        assembly ("memory-safe") {
            let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
            // bubble errors
            if iszero(success) {
                let ptr := mload(0x40)
                returndatacopy(ptr, 0, returndatasize())
                revert(ptr, returndatasize())
            }
            returnSize := returndatasize()
            returnValue := mload(0)
        }

        if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

No need safeMath in solidity ^0.8.0+

Invalid access control or Uninitialized functions in the logic contract

使用确定的循环问题

使用确定的solidity 版本

Preference

https://www.rareskills.io/post/solidity-beginner-mistakes