Solidity 学习笔记

Posted 真·skysys

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Solidity 学习笔记相关的知识,希望对你有一定的参考价值。

  • 主要参考网上资料学习,个人学习笔记有删改,参考出处在文末列出。

0 基础

  1. IDE: remix
  2. Type
  • Bool: bool public _bool = true; 默认false;
  • 整型:int、uint、uint256,默认0;
  • 地址类型:address,分为 payable 和普通地址,payable address 有 .balance 和 .transfer()。二者都有 .call()。默认address(0);
  • 字符串:string,默认"";
  • 字节数组:
    • 定长:byte, bytes1, bytes8, bytes32
    • 不定长:引用类型
  • 枚举类型: enum (几乎没人用)默认第一个元素
   // 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
    enum ActionSet  Buy, Hold, Sell 
    // 创建enum变量 action
    ActionSet action = ActionSet.Buy;

    // enum可以和uint显式的转换
    function enumToUint() external view returns(uint)
        return uint(action);
    
  • 引用类型:array、struct、mapping…
  • 注意:delete a会让 a 变为其类型的默认值;
  • constant & immutable
    • constant:声明的时候就需要指定该关键字;
    • immutable:在声明时或构造函数中初始化;
    • string 和 bytes 可以声明为 constant,但不能为 immutable;

函数 function

function <function name> (<parameter types>) internal|external|public|private [pure|view|payable] [returns (<return types>)]
  • pure & view:与 gas 有关,加上这俩标记的函数不修改链上状态,因此不消耗 gas。
    • 修改状态的操作举例:状态变量、链上event、create contract、selfdestruct、转账、调用其他非pure&view函数、任何低级调用(low-level calls)、使用包含某些opcode的内联汇编。
    • pure:不读也不写
    • view:只读不写
    • 其他:可读可写

pure不读也不写有啥用?举例如下:

 function addPure(uint256 _number) external pure returns(uint256 new_number)
       new_number = _number+1;
 
  • internal|external|public|private:没标明函数类型的,默认internal
    • public: 内部外部均可见。(也可用于修饰状态变量,public变量会自动生成 getter函数,用于查询数值)。
    • private: 只能从本合约内部访问,继承的合约也不能用(也可用于修饰状态变量)。
    • external: 只能从合约外部访问(但是可以用this.f()来调用,f是函数名)。
    • internal: 只能从合约内部访问,继承的合约可以用(也可用于修饰状态变量)。
  • payable:可以在call这个function的时候同时转eth。
 //payable: 递钱,能给合约支付eth的函数
 function testPayable() external payable returns(uint256 balance) 
     balance = address(this).balance; // this -> contract address
 
  • return & returns:returns 在 函数名 后,声明返回类型和变量名。return 在函数主体中,返回指定变量。
  // 返回多个变量
function returnMultiple() public pure returns(uint256, bool, uint256[3] memory)
    return(1, true, [uint256(1),2,5]);

 // 命名式返回
 function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array)
     _number = 2;
     _bool = false; 
     _array = [uint256(3),2,1];
 
// 命名式返回,依然支持return
 function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array)
     return(1, true, [uint256(1),2,5]);
 
  • solidity 支持解构赋值:
        uint256 _number;
        bool _bool;
        uint256[3] memory _array;
        (_number, _bool, _array) = returnNamed();

        (, _bool2, ) = returnNamed(); // 只要部分返回值

存储

对引用类型的变量,由于变量复杂且存储消耗大,因此需要显示声明存储位置。合约中的状态变量默认存储在storage中

  • storage:存储在链上(类似硬盘)消耗 gas 多;
  • memory:临时存在内存中,消耗 gas 小;
  • calldata:临时存在内存中,消耗 gas 小;calldata 不能修改,一般用于函数参数。
    function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata)
        //参数为calldata数组,不能被修改
        // _x[0] = 0 //这样修改会报错
        return(_x);
    

简单来说,相同存储位置的会用引用方式,否则一般是副本。

  1. storage(合约的状态变量)赋值给本地storage(函数里的)时候,会创建引用,改变新变量会影响原变量。
    uint[] x = [1,2,3]; // 状态变量:数组 x,这里是 storage

    function fStorage() public
        //声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x
        uint[] storage xStorage = x;
        xStorage[0] = 100;
    
  1. storage 赋值给 memory 或者 memory 赋值给 storage,会创建副本
    uint[] x = [1,2,3]; // 状态变量:数组 x,这里是 storage
    
    function fMemory() public view
        //声明一个Memory的变量xMemory,复制x。修改xMemory不会影响x
        uint[] memory xMemory = x;
        xMemory[0] = 100;
        xMemory[1] = 200;
        uint[] memory xMemory2 = x;
        xMemory2[0] = 300;
    
  1. memory赋值给memory,会创建引用,改变新变量会影响原变量。
  2. 其他情况,变量赋值给storage,会创建独立的复本,修改其中一个不会影响另一个。

作用域

状态变量(state variable),局部变量(local variable)和全局变量(global variable) 作用域。

1.状态变量
状态变量数据存储在链上,所有合约内函数都可以访问 ,gas消耗高。状态变量在合约内、函数外声明:

contract Variables 
    uint public x = 1;
    uint public y;
    string public z;
	...
    function foo() external
        // 可以在函数里更改状态变量的值
        x = 5;
        y = 2;
        z = "0xAA";
    

2.局部变量:略。
3.全局变量:solidity 预留关键字的全局变量,可以在函数内不声明直接使用。

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

引用类型

Array

  • solidity中如果一个值没有指定type的话,默认就是最小单位的该type。如果指定,元素的type是以第一个元素为准。
// 固定长度
    uint[8] array1;
    bytes1[5] array2;
    address[100] array3;

// 可变长长度
    uint[] array4;
    bytes1[] array5;
    address[] array6;
    bytes array7;

// memory动态数组:memory修饰的动态数组,可以用new操作符来创建,但是必须声明长度,并且声明后长度不能改变
    uint[] memory array8 = new uint[](5);
    bytes memory array9 = new bytes(9);
// 如果创建的是动态数组,需要一个一个元素的赋值。
  	uint[] memory x = new uint[](3);
    x[0] = 1;
    x[1] = 3;
    x[2] = 4;

// 数组字面常数(Array Literals)是写作表达式形式的数组,用方括号包着来初始化array的一种方式,并且里面每一个元素的type是以第一个元素为准的
pragma solidity >=0.4.16 <0.9.0;
contract C 
    function f() public pure 
        g([uint(1), 2, 3]); // 元素的type是以第一个元素为准
    
    function g(uint[3] memory) public pure 
        // ...
    

  • length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。
  • push(): 动态数组和bytes拥有push(),在数组最后添加一个0元素。
  • push(x): 动态数组和bytes拥有push(x),数组最后添加一个x元素。
  • pop(): 动态数组和bytes拥有pop()成员,移除数组最后一个元素。

struct

    // 结构体
    struct Student
        uint256 id;
        uint256 score; 
    

    Student student; // 初始一个student结构体

    //  给结构体赋值
    // 方法1:在函数中创建一个storage的struct引用
    function initStudent1() external
        Student storage _student = student;
        _student.id = 11;
        _student.score = 100;
    
    // 方法2:直接引用状态变量的struct
    function initStudent2() external
        student.id = 1;
        student.score = 80;
    

映射类型 mapping

mapping(_KeyType => _ValueType)

  • 规则1:_KeyType 只能选择 solidity 默认的类型,_ValueType 可以用自定义类型。
  • 规则2:映射的存储位置必须是 storage,可用于合约的状态变量、函数中的 storage 变量和library函数的参数。不能用于 public 函数的参数或返回结果中,因为 mapping 记录的是一种关系 (key - value pair)。
  • 规则3:如果映射声明为 public,那么 solidity 会自动创建一个 getter,可以通过 Key 来查询对应的 Value。
  • Ethereum 会定义所有未使用的空间为0,所以未赋值(Value)的键(Key)初始值都是0。映射使用 keccak256(key) 当成 offset 存取 value。映射不储存任何键(Key)的信息,也没有 length 的信息。

控制流程

function ifElseTest(uint256 _number) public pure returns(bool)
    if(_number == 0)
	    return(true);
    else
	    return(false);
    


function forLoopTest() public pure returns(uint256)
    uint sum = 0;
    for(uint i = 0; i < 10; i++)
	    sum += i;
    
    return(sum);


function whileTest() public pure returns(uint256)
    uint sum = 0;
    uint i = 0;
    while(i < 10)
	    sum += i;
	    i++;
    
    return(sum);


function doWhileTest() public pure returns(uint256)
    uint sum = 0;
    uint i = 0;
    do
	    sum += i;
	    i++;
    while(i < 10);
    return(sum);


function ternaryTest(uint256 x, uint256 y) public pure returns(uint256)
    // return the max of x and y
    return x >= y ? x: y; 

插入排序 solidity 版【有坑需注意】

# python code
def insertionSort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i-1
        while j >=0 and key < arr[j] :
                arr[j+1] = arr[j]
                j -= 1
        arr[j+1] = key

直接翻译:

    // 插入排序 错误版 ERROR
    function insertionSortWrong(uint[] memory a) public pure returns(uint[] memory) 
        for (uint i = 1;i < a.length;i++)
            uint temp = a[i];
            uint j=i-1;
            while( (j >= 0) && (temp < a[j]))
                a[j+1] = a[j];
                j--; // ERROR:j有可能会取到-1 但这里j是uint..报错!
            
            a[j+1] = temp;
        
        return(a);
    
    // 插入排序 正确版
    function insertionSort(uint[] memory a) public pure returns(uint[] memory) 
        // note that uint can not take negative value
        for (uint i = 1;i < a.length;i++)
            uint temp = a[i];
            uint j=i;	// 注意这里,一个重要的小细节
            while( (j >= 1) && (temp < a[j-1])) // 又一个小细节
                a[j] = a[j-1];
                j--;
            
            a[j] = temp;
        
        return(a);
    

构造函数和修饰器

  • Ownable、constructor、modifier
  • constructor:每个合约可以定义一个,并在部署时自动运行一次,用来初始化合约参数,如初始化合约的owner地址。
   address owner; // 定义owner变量

   // 构造函数
   constructor() 
      owner = msg.sender; // 在部署合约的时候,将owner设置为部署者的地址
   


 // 旧版构造函数:version<0.4.22
   pragma solidity =0.4.21;
	contract Parents 
	    // 与合约名Parents同名的函数就是构造函数
	    function Parents () public  // parents 就不是构造函数!
	    
	

注意: 构造函数在不同的 solidity 版本中的语法并不一致,在Solidity 0.4.22之前,构造函数不使用 constructor 而是使用与合约名同名的函数作为构造函数而使用,由于这种旧写法容易使开发者在书写时发生疏漏(例如合约名叫 Parents,构造函数名写成 parents),使得构造函数变成普通函数,引发漏洞,所以0.4.22版本及之后,采用了全新的 constructor 写法。

  • modifier:
   // 定义modifier
   modifier onlyOwner 
      require(msg.sender == owner); // 检查调用者是否为owner地址
      _; // 如果是的话,继续运行函数主体;否则报错并revert交易
   


   function changeOwner(address _newOwner) external onlyOwner
      owner = _newOwner; // 只有owner地址运行这个函数,并改变owner
   

事件

链上 event 的特点:1. 应用程序(ether.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。2. 事件是EVM上比较经济的存储数据的方式,每个大概消耗 2,000 gas;相比之下,链上存储一个新变量至少需要 20,000 gas。

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

    // 定义_transfer函数,执行转账逻辑
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) external 

        _balances[from] = 10000000; // 给转账地址一些初始代币

        _balances[from] -=  amount; // from地址减去转账数量
        _balances[to] += amount; // to地址加上转账数量

        // 释放事件
        emit Transfer(from, to, amount);
    
  • indexed: 表示检索事件的key,在链上会对key单独索引和存储。每个事件最多带有3个indexed key。每个indexed variable固定256bits
  • event 的哈希以及这三个带 indexed 的变量在 EVM 日志中通常被存储为 topic。其中topic[0]是此事件的 keccak256 哈希,topic[1]~topic[3]存储了带 indexed 变量的 keccak256 哈希。
  • value 不带 indexed 关键字,会存储在事件的 data 部分中,可以理解为事件的“值”。data 部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 data 部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了 256 比特,即使存储在事件的 topic 部分中,也是以哈希的方式存储。另外,data 部分的变量在存储上消耗的 gas 相比于 topic 更少。
  • event 是链上分析工具如 Nansen、Dune Analysis 的基础。

继承

  • virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。
  • override:子合约重写了父合约中的函数,需要加上override关键字。
contract Yeye 
    event Log(string msg);

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

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

    function yeye() public virtual 
        emit Log("Yeye");
    


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

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

    function baba() public virtual
        emit Log("Baba");
    

  • 多重继承:继承时要按辈分最高到最低的顺序排。 如果某一个函数在多个继承的合约里都存在,比如例子中的hip()和pop(),在子合约里必须重写,不然会报错。 重写在多个父合约中都重名的函数时,override关键字后面要加上所有父合约名字,例如override(Yeye, Baba)。 例子:
contract Erzi is Yeye, Baba
    // 继承两个function: hip()和pop(),输出值为Erzi。
    function hip() public virtual override(Yeye, Baba)
        emit Log("Erzi");
    

    function pop() public virtual override(Yeye, Baba) 
        emit Log("Erzi");
    

修饰器的继承

contract Base1 
    modifier exactDividedBy2And3(uint _a) virtual 
        require(_a % 2 == 0 && _a % 3 == 0);
        _;
    


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

    //计算一个数分别被2除和被3除的值
    function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint)
        uint div2 = _dividend / 2;
        uint div3 = _dividend / 3;
        return (div2, div3);
    

构造函数的继承

// 构造函数的继承
abstract contract A 
    uint public a;

    constructor(uint _a) 
        a = _a;
    

方式一:在继承时声明父构造函数的参数,例如:contract B is A(1)。
方式二:在子合约的构造函数中声明构造函数的参数。

contract C is A 
    constructor(uint _c) A(_c * _c) 

调用父合约的函数

1.直接调用:子合约可以直接用父合约名.函数名()的方式来调用父合约函数
2.super.函数名()来调用最近的父合约函数。solidity继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba,那么Baba是最近的父合约,super.pop()将调用Baba.pop()而不是Yeye.pop()

    function callParent() public
        Yeye.pop();
    
    function callParentSuper() public
        // 将调用最近的父合约函数,Baba.pop()
        super.pop();
    

抽象合约和接口

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

  • 接口:1.不能包含状态变量;2.不能包含构造函数;3.不能继承除接口外的其他合约;4.所有函数都必须是external且不能有函数体;5.继承接口的合约必须实现接口定义的所有功能;
  • 接口提供了两个重要的信息:1.合约里每个函数的bytes4选择器,以及基于它们的函数签名函数名(每个参数类型)。2.接口id(EIP165)。接口与合约ABI(Application Binary Interface)等价,可以相互转换:编译接口可以得到合约的ABI,利用abi-to-sol工具也可以将ABI json文件转换为接口sol文件。(这里所谓的选择器类似一个offset定位到这个函数)
interface IERC721 is IERC165 
// Transfer事件:在转账时被释放,记录代币的发出地址from,接收地址to和tokenid。
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
// Approval事件:在授权时释放,记录授权地址owner,被授权地址approved和tokenid。
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
// ApprovalForAll事件:在批量授权时释放,记录批量授权的发出地址owner,被授权地址operator和授权与否的approved。
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
// balanceOf:返回某地址的NFT持有量balance。
    function balanceOf(address owner) external view returns (uint256 balance);
// ownerOf:返回某tokenId的主人owner。
    function ownerOf(uint256 tokenId) external view returns (address owner);
// transferFrom:普通转账,参数为转出地址from,接收地址to和tokenId。
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
// safeTransferFrom:安全转账(如果接收方是合约地址,会要求实现ERC721Receiver接口)。参数为转出地址from,接收地址to和tokenId。
    function transferFrom(address from, address to, uint256 tokenId) external;
// approve:授权另一个地址使用你的NFT。参数为被授权地址approve和tokenId。
    function approve(address to, uint256 tokenId) external;
// getApproved:查询tokenId被批准给了哪个地址。
    function getApproved(uint256 tokenId) external view returns (address operator);
// setApprovalForAll:将自己持有的该系列NFT批量授权给某个地址operator。
    function setApprovalForAll(address operator, bool _approved) external;
// isApprovedForAll:查询某地址的NFT是否批量授权给了另一个operator地址。
    function isApprovedForAll(address owner, address operator) external view returns (bool);
// safeTransferFrom:安全转账的重载函数,参数里面包含了data。
    function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external;

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

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

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

异常

solidity三种抛出异常的方法:error,require和assert,并比较三种方法的gas消耗。(require>assert>error)

  • Error(ver>0.8支持),省gas
error TransferNotOwner(); // 自定义error

    function transferOwner1(uint256 tokenId, address newOwner) public 
        if(_owners[tokenId] != msg.sender)
            revert TransferNotOwner();
        
        _owners[tokenId] = newOwner;
    
  • Require:(ver<0.8之前常用方法,之后也支持) 缺点:gas与描述异常的字符串长度有关
    function transferOwner2(uint256 tokenId, address newOwner) public 
        require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
        _owners[tokenId] = newOwner;
    
  • Assert:
    function transferOwner3(uint256 tokenId, address newOwner) public 
        assert(_owners[tokenId] == msg.sender);
        _owners[tokenId] = newOwner;
    

1 进阶

函数重载

  • solidity 允许函数重载,不允许修饰器(modifier)重载。重载函数在经过编译器编译后,由于不同的参数类型,都变成了不同的函数选择器(selector)。
function saySomething() public pure returns(string memory)
    return("Nothing");


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

错误示例:

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

    function f(uint256 _in) public pure returns (uint256 out) 
        out = _in;
    
// 调用f(50),因为50既可以被转换为uint8,也可以被转换为uint256,因此会报错。

库合约和普通合约区别:1.不能存在状态变量 2.不能够继承或被继承 3.不能接收以太币 4.不可以被销毁。

  • String库合约:将 uint256 类型转换为相应的 string 类型的代码库
library Strings 
    bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";

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

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

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

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

  • 如何使用库合约:
    • 方式一:using for。用于附加库函数(从库 A)到任何类型(B)。添加完指令后,库A中的函数会自动添加为B类型变量的成员,可以直接调用。注意:在调用的时候,这个变量会被当作第一个参数传递给函数:
    • 方式二:库合约名称调用库函数
    // 利用using for指令
    using Strings for uint256;
    function getString1(uint256 _number) public pure returns(string memory)
        // 库函数会自动添加为uint256型变量的成员
        return _number.toHexString();
    

    // 直接通过库合约名调用
    function getString2(uint256 _number) public pure returns(string memory)
        return Strings.toHexString(_number);
    

常规的库:

  • String:将uint256转换为String
  • Address:判断某个地址是否为合约地址
  • Create2:更安全的使用Create2 EVM opcode
  • Arrays:跟数组相关的库函数

Import

文件结构
├── Import.sol
└── Yeye.sol

// 通过文件相对位置import
import './Yeye.sol';
import Yeye from './Yeye.sol';

// 通过网址引用
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
// 通过npm的目录导入:
import '@openzeppelin/contracts/access/Ownable.sol';

接收和发送ETH

Solidity支持两种特殊的回调函数,receive() 和 fallback(),他们主要在两种情况下被使用:1)接收ETH;2)处理合约中不存在的函数调用(代理合约proxy contract)。
注意⚠️:在solidity 0.6.x版本之前,语法上只有 fallback() 函数,用来接收用户发送的ETH时调用以及在被调用函数签名没有匹配到时,来调用。 0.6版本之后,solidity才将 fallback() 函数拆分成 receive() 和 fallback() 两个函数。

  • 接收ETH函数 receive:receive()函数不能有任何的参数,不能返回任何值,必须包含external和payable。
    // 定义事件
    event Received(address Sender, uint Value);
    // 接收ETH时释放Received事件
    receive() external payable 
        emit Received(msg.sender, msg.value);
    
  • 当合约接收ETH的时候,receive()会被触发。receive()最好不要执行太多的逻辑因为如果别人用send和transfer方法发送ETH的话,gas会限制在2300receive()太复杂可能会触发Out of Gas报错;如果用call就可以自定义gas执行更复杂的逻辑。
  • 有些恶意合约会在receive() 函数(老版本的是 fallback() 函数)嵌入恶意消耗gas的内容或者使得执行故意失败的代码,导致一些包含退款和转账逻辑的合约不能正常工作,因此写包含退款等逻辑的合约时候,一定要注意这种情况。
  • 回退函数 fallback() 函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约 proxy contract。fallback() 声明时不需要 function 关键字,必须由 external 修饰,一般也会用 payable 修饰,用于接收 ETH:fallback() external payable … 。
    // fallback
    fallback() external payable
        emit fallbackCalled(msg.sender, msg.value, msg.data);
    
  • receive & fallback 触发逻辑:
触发fallback() 还是 receive()?
           接收ETH
              |
         msg.data是空?
            /  \\
          是    否
          /      \\
receive()存在?   fallback()
        / \\
       是  否
      /     \\
receive()   fallback()
  • transfer & send & call: 鼓励用 call
    • transfer:接收方地址.transfer(发送ETH数额);gas 限制是 2300,足够用于转账,但对方合约的 fallback() 或 receive() 函数不能实现太复杂的逻辑;转账失败,会自动 revert
    • send:接收方地址.send(发送ETH数额);gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑;如果转账失败,不会revertsend()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下; (几乎没人用)
    • call:接收方地址.callvalue: 发送ETH数额(""),没有gas限制,可以支持对方合约fallback()或receive()函数实现复杂逻辑,转账失败,不会revert,返回值是(bool, data)。
contract ReceiveETH 
    // 收到eth事件,记录amount和gas
    event Log(uint amount, uint gas);
    
    // receive方法,接收eth时被触发
    receive() external payable
        emit Log(msg.value, gasleft());
    
    
    // 返回合约ETH余额
    function getBalance() view public returns(uint) 
        return address(this).balance;
    


contract SendETH 
    // 构造函数,payable使得部署的时候可以转eth进去
    constructor() payable
    // receive方法,接收eth时被触发
    receive() external payable

// 用transfer()发送ETH
	function transferETH(address payable _to, uint256 amount) external payable
	    _to.transfer(amount);
	



// send()发送ETH
function sendETH(address payable _to, uint256 amount) external payable
    // 处理send的返回值
    bool success = _to.send(amount);
    if(!success)
        revert SendFailed();
    


// call()发送ETH
function callETH(address payable _to, uint256 amount) external payable
    // 处理下call的返回值,如果失败,revert交易并发送error
    (bool success,) = _to.callvalue: amount("");
    if(!success)
        revert CallFailed();
    

调用其他合约

// 目标合约
contract OtherContract 
    uint256 private _x = 0; // 状态变量_x
    // 收到eth的事件,记录amount和gas
    event Log(uint amount, uint gas);
    
    // 返回合约ETH余额
    function getBalance() view public returns(uint) 
        return address(this).balance;
    

    // 可以调整状态变量_x的函数,并且可以往合约转ETH (payable)
    function setX(uint256 x) external payable
        _x = x;
        // 如果转入ETH,则释放Log事件
        if(msg.value > 0)
            emit Log(msg.value, gasleft());
        
    

    // 读取_x
    function getX() external view returns(uint x)
        x = _x;
    


// 发起调用的合约
contract CallContract
   function callSetX(address _Address, uint256 x) external
        OtherContract(_Address).setX(x); // 在函数里传入目标合约地址,生成目标合约的引用,然后调用目标函数。
    
    function callGetX(OtherContract _Address) external view returns(uint x) // 合约变量
        x = _Address.getX();
    
    function callGetX2(address _Address) external view returns(uint x)
        OtherContract oc = OtherContract(_Address); // 合约变量
        x = oc.getX();
    
      function setXTransferETH(address otherContract, uint256 x) payable external
        OtherContract(otherContract).setXvalue: msg.value(x); // wei
    

  • 如果目标合约的函数是payable的,那么我们可以通过调用它来给合约转账:_Name(_Address).fvalue: _Value()。wei 为单位。

call、delegatecall

call 是 address 类型的低级成员函数,用来与其他合约交互。它的返回值为(bool, data),分别对应call是否成功以及目标函数的返回值。

二进制编码参数 = abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)
abi.encodeWithSignature("f(uint256,address)", _x, _addr)
目标合约地址.call(二进制编码);
目标合约地址.callvalue:发送数额, gas:gas数额(二进制编码);
  • call 是 solidity 官方推荐的通过触发 fallback 或 receive 函数发送 ETH 的方法。不推荐用 call 来调用另一个合约,因为当你调用不安全合约的函数时,你就把主动权交给了它。推荐的方法仍是声明合约变量后调用函数。
  • 当不知道对方合约的源代码或 ABI,就没法生成合约变量;这时仍可以通过 call 调用对方合约的函数。
contract OtherContract 
    uint256 private _x = 0; // 状态变量x
    // 收到eth的事件,记录amount和gas
    event Log(uint amount, uint gas);
    
    fallback() external payable

    // 返回合约ETH余额
    function getBalance() view public returns(uint) 
        return address(this).balance;
    

    // 可以调整状态变量_x的函数,并且可以往合约转ETH (payable)
    function setX(uint256 x) external payable
        _x = x;
        // 如果转入ETH,则释放Log事件
        if(msg.value > 0)
            emit Log(msg.value, gasleft());
        
    

    // 读取x
    function getX() external view returns(uint x)
        x = _x;
    


contract test 
	// 定义 Response 事件,输出 call 返回的结果 success 和 data
	event Response(bool success, bytes data);
	function callSetX(address payable _addr, uint256 x) public payable 
	    // call setX(),同时可以发送ETH
	    (bool success, bytes memory data) = _addr.callvalue: msg.value(
	        abi.encodeWithSignature("setX(uint256)", x)
	    );
	    emit Response(success, data); //释放事件
	

	function callGetX(address _addr) external returns(uint256)
	    // call getX()
	    (bool success, bytes memory data) = _addr.call(
	        abi.encodeWithSignature("getX()")
	    );
	
	    emit Response(success, data); //释放事件
	    return abi.decode(data, (uint256));
	

// 如果我们给call输入的函数不存在于目标合约,那么目标合约的fallback函数会被触发。
  • Delegatecall委托调用与call的区别:委托调用改变的是B的状态。delegatecall 在调用合约时可以指定交易发送的 gas,但不能指定发送的ETH数额

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

注意:delegatecall有安全隐患,使用时要保证当前合约和目标合约的状态变量存储结构相同,并且目标合约安全,不然会造成资产损失。
使用场景:
1.代理合约(Proxy Contract):将智能合约的存储合约和逻辑合约分开:代理合约(Proxy Contract)存储所有相关的变量,并且保存逻辑合约的地址;所有函数存在逻辑合约(Logic Contract)里,通过delegatecall执行。当升级时,只需要将代理合约指向新的逻辑合约即可。
2.EIP-2535 Diamonds(钻石):钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石是具有多个实施合同的代理合同。

// 被调用的合约C
contract C 
    uint public num;
    address public sender;

    function setVars(uint _num) public payable 
        num = _num;
        sender = msg.sender;
    

// 合约B必须和目标合约C的变量存储布局必须相同:变量名可以不同,但变量类型和声明顺序必须相同
contract B 
    uint public num;
    address public sender;

    // 通过call来调用C的setVars()函数,将改变合约C里的状态变量
    function callSetVars(address _addr, uint _num) external payable
        // call setVars()
        (bool success, bytes memory data) = _addr.call(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
    

    // 通过delegatecall来调用C的setVars()函数,将改变合约B里的状态变量
    function delegatecallSetVars(address _addr, uint _num) external payable
	        // delegatecall setVars()
	        (bool success, bytes memory data) = _addr.delegatecall(
	            abi.encodeWithSignature("setVars(uint256)", _num)
	        );
	    
	

在合约中创建新合约:create、create2

Contract x = new Contractvalue: _value(params)

简易的 Uniswap 代码:Uniswap V2 中包含了

  • UniswapV2Pair: 币对合约,用于管理币对地址、流动性、买卖。
  • UniswapV2Factory: 工厂合约,用于创建新的币对,并管理币对地址。
contract Pair
    address public factory; // 工厂合约地址
    address public token0; // 代币1
    address public token1; // 代币2

    constructor() payable 
        factory = msg.sender;
    

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

提问:为什么uniswap不在constructor中将token0和token1地址更新好?
答:因为uniswap使用的是create2创建合约,限制构造函数不能有参数。当使用create时,Pair合约允许构造函数有参数,可以在constructor中将token0和token1地址更新好。

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

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

  • CREATE2 操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址。Uniswap 创建 Pair 合约用的就是 CREATE2 而不是 CREATE。
  • CREATE 如何计算地址
    智能合约可以由其他合约和普通账户利用 CREATE 操作码创建。 在这两种情况下,新合约的地址都以相同的方式计算:新地址 = hash(创建者地址, nonce)
  • CREATE2如何计算地址
    CREATE2的目的是为了让合约地址独立于未来的事件。不管未来区块链上发生了什么,你都可以把合约部署在事先计算好的地址上(与nonce无关):新地址 = hash("0xFF",创建者地址, salt, bytecode)
Contract x = new Contractsalt: _salt, value: _value(params)
  • 基于 create2 的 uniswap demo:
contract PairFactory2
        mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
        address[] public allPairs; // 保存所有Pair地址

        function createPair2(address tokenA, address tokenB) external returns (address pairAddr) 
            require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突
            // 计算用tokenA和tokenB地址计算salt
            (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序
            bytes32 salt = keccak256(abi.encodePacked(token0, token1));
            // 用create2部署新合约
            Pair pair = new Pairsalt: salt(); 
            // 调用新合约的initialize方法
            pair.initialize(tokenA, tokenB);
            // 更新地址map
            pairAddr = address(pair);
            allPairs.push(pairAddr);
            getPair[tokenA][tokenB] = pairAddr;
            getPair[tokenB][tokenA] = pairAddr;
        

        // 提前计算pair合约地址
        function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress)
            require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突
            // 计算用tokenA和tokenB地址计算salt
            (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序
            bytes32 salt = keccak256(abi.encodePacked(token0, token1));
            // 计算合约地址方法 hash()
            predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
                bytes1(0xff),
                address(this),
                salt,
                keccak256(type(Pair).creationCode)
            )))));
        

CREATE2 让我们可以在部署合约前确定它的合约地址,这是一些 Layer2 项目的基础。

删除合约

selfdestruct 命令可以用来删除智能合约,并将该合约剩余ETH转到指定地址。selfdestruct 是为了应对合约出错的极端情况而设计的。它最早被命名为 suicide,但是这个词太敏感。为了保护抑郁的程序员,改名为 selfdestruct。

contract DeleteContract 

    uint public value = 10;

    constructor() payable 

    receive() external payable 

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

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

  • 当合约被销毁后与智能合约的交互也能成功,并且返回0。

ABI 编码解码

ABI (Application Binary Interface,应用二进制接口) 是与以太坊智能合约交互的标准。数据基于他们的类型编码;并且由于编码后不包含类型信息,解码时需要注明它们的类型。 Solidity中,ABI编码有4个函数:abi.encode, abi.encodePacked, abi.encodeWithSignature, abi.encodeWithSelector。而ABI解码有1个函数:abi.decode,用于解码abi.encode的数据。

// 与合约交互采用的方案:
    function encode() public view returns(bytes memory result) 
        result = abi.encode(x, addr, name, array); // 每个参数填充为32字节(256bits),不够的用前导0填充
    
 // 省空间,不与合约交互,比如组合起来算hash
    function encodePacked() public view returns(bytes memory result) 
        result = abi.encodePacked(x, addr, name, array); // 根据每个参数所需最低空间编码,
    
// 与 encode 类似,用于与合约交互,等同于在abi.encode编码结果前加上了4字节的函数选择器
    function encodeWithSignature() public view returns(bytes memory result) 
        result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
    
// 与abi.encodeWithSignature功能类似,只不过第一个参数为函数选择器,为函数签名Keccak哈希的前4个字节
    function encodeWithSelector() public view returns(bytes memory result) 
        result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
    


// 解码
    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]));
    
  • 使用场景:在合约开发中,ABI常配合call来实现对合约的底层调用
    bytes4 selector = contract.getValue.selector;

    bytes memory data = abi.encodeWithSelector(selector, _x);
    (bool success, bytes memory returnedData) = address(contract).staticcall(data);
    require(success);

    return abi.decode(returnedData, (uint256));
  • 使用场景:ethers.js中常用ABI实现合约的导入和函数调用。
    const wavePortalContract = new ethers.Contract(contractAddress, contractABI, signer);
    const waves = await wavePortalContract.getAllWaves();
  • 使用场景:对不开源合约进行反编译后,某些函数无法查到函数签名,可通过ABI进行调用。

Hash

Hash 的抗碰撞性:

  • 弱抗碰撞性:给定一个消息x,找到另一个消息x’使得hash(x) = hash(x’)是困难的。
  • 强抗碰撞性:找到任意x和x’,使得hash(x) = hash(x’)是困难的。

Keccak256和sha3:
sha3由keccak标准化而来,在很多场合下Keccak和SHA3是同义词,但在2015年8月SHA3最终完成标准化时,NIST调整了填充算法。所以SHA3就和keccak计算的结果不一样,这点在实际开发中要注意。

以太坊在开发的时候sha3还在标准化中,所以采用了keccak,所以Ethereum和Solidity智能合约代码中的SHA3是指Keccak256,而不是标准的NIST-SHA3,为了避免混淆,直接在合约代码中写成Keccak256是最清晰的。

选择器 Selector

调用智能合约时,本质上是向目标合约发送了一段calldata。calldata中前4个字节是selector(函数选择器)。

  • msg.data是solidity中的一个全局变量,值为完整的calldata(调用函数时传入的数据)。
    // event 返回msg.data
    event Log(bytes data);

    function mint(address to) external
        emit Log(msg.data);
    
/*
	params: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
	calldata: 0x6a6278420000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78

	前4个字节为函数选择器selector:
	0x6a627842
	// method id定义为函数签名的Keccak哈希后的前4个字节,当selector与method id相匹配时,即表示调用该函数
	后面32个字节为输入的参数:
	0x0000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78
*/
    function callWithSignature() external returns(bool, bytes memory)
        (bool success, bytes memory data) = address(this).call(abi.encodeWithSelector(0x6a627842, "0x2c44b726ADF1963cA47Af88B284C06f30380fC78"));
        return(success, data);
    

try-catch

  • 版本:solidity0.6+后支持
  • 在solidity中,try-catch只能被用于external函数或创建合约时constructor(被视为external函数)的调用。基本语法如下:
        try externalContract.f() 
            // call成功的情况下 运行一些代码
         catch 
            // call失败的情况下 运行一些代码
        

// 可以使用this.f()来替代externalContract.f(),this.f()也被视作为外部调用,但不可在构造函数中使用,因为此时合约还未创建。


        try externalContract.f() returns(returnType)
            // call成功的情况下 运行一些代码
         catch Error(string memory reason) 
            // 捕获失败的 revert() 和 require()
         catch (bytes memory reason) 
            // 捕获失败的 assert()
        

demo:

contract OnlyEven
    constructor(uint a)
        require(a != 0, "invalid number");
        assert(a != 1);
    

    function onlyEven(uint256 b) external pure returns(bool success)
        // 输入奇数时revert
        require(b % 2 == 0, "Ups! Reverting");
        success = true;
    


contract demo
    // 成功event
    event SuccessEvent();

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

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

    constructor() 
        even = new OnlyEven(2);
    
    // 在external call中使用try-catch
    function execute(uint amount) external returns (bool success) 
        try even.onlyEven(amount) returns(bool _success)
            // call成功的情况下
            emit SuccessEvent();
            return _success;
         catch Error(string memory reason)
            // call不成功的情况下
            emit CatchEvent(reason);
        
    

    // 在创建新合约中使用try-catch (合约创建被视为external call)
    // executeNew(0)会失败并释放`CatchEvent`
    // executeNew(1)会失败并释放`CatchByte`
    // executeNew(2)会成功并释放`SuccessEvent`
    function executeNew(uint a) external returns (bool success) 
        try new OnlyEven(a) returns(OnlyEven _even)
            // call成功的情况下
            emit SuccessEvent();
            success = _even.onlyEven(a);
         catch Error(string memory reason) 
            // catch失败的 revert() 和 require()
            emit CatchEvent(reason);
         catch (bytes memory reason) 
            // catch失败的 assert()
            emit CatchByte(reason);
        
    


2 应用(本文略)

考虑本文已经过长了,应用和安全的部分在后续的博客中列出。

Reference

  1. 官方文档中文版
  2. up主 崔棉大师
  3. wtf academy
  4. WTFSolidity github

区块链Solidity智能合约语言学习笔记

Solidity简介

以太坊拥有多种高级语言,可用于编写智能合约,每种语言都受到另一种广泛使用的语言的启发。最流行的一种叫做Solidity,它基于JavaScript。由于Solidity是迄今为止最成熟的以太坊语言,因此它是社区大力鼓励开发人员现在使用的语言。

Solidity是一种语法类似JavaScript的高级语言。它被设计成以编译的方式生成以太坊虚拟机代码。使用它很容易创建用于投票、众筹、封闭拍卖、多重签名钱包等等的合约。

编译环境

Remix在线编译器: http://remix.app.hubwiz.com/

VScode编译器也行,其插件:

                                        

第一个程序Hello world

参考:https://blog.csdn.net/weixin_45067603/article/details/105726491

pragma solidity ^0.4.0;

contract helloworld 
    string myword = "helloworld";
    
    function show() public view returns(string)
        return myword;
    
    
    function changMyword(string _newWord) public
        myword = _newWord;
    


solidity的四种可见度/访问权限

  • public:任何人都可以调用该函数,包括DApp的使用者。
  • private:只有合约本身可以调用该函数(在另一个函数中)。
  • internal:只有这份合同以及由此产生的所有合同才能称之为合同。
  • external:只有外部可以调用该函数,而合约内部不能调用。

solidity的三种修饰符

view: 可以自由调用,因为它只是“查看”区块链的状态而不改变它

pure: 也可以自由调用,既不读取也不写入区块链

payable:常常用于将代币发送给合约地址。

solidity的函数应有以下部分组成

function

你的函数名字(类型1 名字,类型2 名字,。。。,类型n 名字) 如果没有就什么都不填即可

可见度/访问权限,即public/private/internal/external 如果不写则系统默认为public并且警告

修饰符,即view/pure/payable 如果需要花费gas则不写

returns(类型1 名字,类型2 名字,。。。,类型n 名字)

布尔值bool

pragma solidity ^0.4.0;

contract helloworld 
    bool boola=true; //声明一个布尔类型的值,只用一个等号
    function booltesta() public view returns(bool)
        return boola;
    
    
    function booltestb(int a,int b) public pure returns(bool)
        return a==b;
    
    
    function boolteston(int a,int b) public pure returns(bool)
        return !(a==b);
    

  

与,或

即: &&  ||

通常运算符

即+,-,*,/,%以及特殊的符号**代表x的x次幂

位运算符

1.& 操作数之间转换成二进制之后每位进行与运算操作(同1取1)

2.| 操作数之间转换成二进制之后每位进行或运算操作(有1取1)

3.~ 操作数转换成二进制之后每位进行取反操作(直接相反)

4.^ 操作数之间转换成二进制之后每位进行异或操作(不同取1)

5.<<操作数转换成二进制之后每位向左移动x位的操作

6.>>操作数转换成二进制之后每位向右移动x位的操作

可变长度byte数组

声明方法

bytes arr = new bytes(length);

function arrlengthchange(uint a) public  arr1.length=a;  

String型

pragma solidity ^0.4.0;
contract stringtest1
    string testword='helloworld'; //68656c6c6f776f726c64为这串字符的16进制
    
    function stringlength() public view returns (uint)
        //return testword.length;  直接返回长度会报错
        return bytes(testword).length;  //强制类型转换之后可以
    
    
    function stringchange() public  
        //testword[0]='A'; 直接进行变更会报错
        bytes(testword)[0]='A'; //41为A的16进制值,强制类型转换之后可以修改
    
    
    function getname() public view returns(bytes)
        return bytes(testword); //查看16进制的string
    
    
    function stringchangetest() public view returns(byte)
        return bytes(testword)[0]; //查看第一位是否被修改
    
    

string类型是怎么存储数据的

英文字符(A-z)以及特殊字符(&*@#&%())等为一个字节
中文字符为3个字节 PS.solidity中文输入支持很差

pragma solidity ^0.4.0;

contract stringstoragetest
    string testword1='asldhlkasdh';
    string testword2='^*^*%*()*-/';
    string testword3='中文字符';
    string testword4='中文字符6666';
    
    function getest1() public view returns(uint)
        return bytes(testword1).length;
    
    function getest2() public view returns(uint)
        return bytes(testword2).length;
    
    function getest3() public view returns(uint)
        return bytes(testword3).length;
    
     function getest4() public view returns(uint)
        return bytes(testword4).length;
    

  

固定长度字节数组bytes的截断 

结论:位数足够则保留前面的,位数不够再后面加0

pragma solidity ^0.4.0;

contract bytetest1
    bytes10 testword=0x68656c6c6f776f726c64; //helloworld
    function transbytes1() public view returns(bytes1)
        return bytes1(testword);
    
    function transbytes2() public view returns(bytes5)
        return bytes5(testword);
    
    function transbytes3() public view returns(bytes12)
        return bytes12(testword);
       

固定长度数组入门

结论:
1.如果不赋值,那么默认所有位均为0
2.支持直接使用.length查看数组长度,但不支持对数组长度做修改
3.不支持通过.push添加数据

pragma solidity ^0.4.0;

contract fixedarrtest
    uint[3] testarr1;//不进行赋值直接声明数组
    uint[3] testarr2=[1,2,3];//声明数组并进行赋值
    
    function showarr1() public view returns(uint[3])
        return testarr1; //如果不赋值,那么默认所有位均为0
    
    
    function showarr2() public view returns(uint[3])
        return testarr2;
    
    
    function initarr() public
        testarr1[0]=12;//进行赋值操作
    
    
    function lengthtest() public view returns(uint)
        return testarr1.length;//solidity支持直接查看数组长度
    
    
    function changelengthtest() public view returns(uint)
        //testarr1.length=testarr1.length+3;//solidity不支持直接修改数组长度
    
    
    function pushtest() public view 
        //testarr2.push(2);//solidity不支持直接push数据到数组
    
    

可变长度数组入门

注意:与固定数组区别在于创建时是否定义长度
结论:
1.如果不初始化就无法单独赋值,但可以push或者改长度使有值之后再进行赋值。即必须修改的这一位不能为空 
2.支持直接使用.length查看数组长度,也支持对数组长度做修改。
将数组长度缩小则会从前往后保留
将数组长度增长则后面原本没有值的位会被默认置0
3.支持直接通过.push方法在末尾添加数值

pragma solidity ^0.4.0;

contract dynamicarrtest
    uint[] testarr=[1,2,3,4,5];
    
    function showarr() public view returns (uint[])
        return testarr;
    
    
    function changearr() public
        //for(uint i=0;i<5;i++)
            testarr[0] = 2;//如果不使0位有值,那么该函数无用
        //
    
    
    function lengthtest() public view returns(uint)
        return testarr.length;
    
    
    function changelengthtest1() public
        testarr.length=1;
    
    
    function changelengthtest2() public
        testarr.length=10;//变长之后后面默认置0
    
    
    function pushtest() public
        testarr.push(6);//可变数组支持此操作
    
    

 固定长度二维数组


结论
1.初始化时,uint[ i ] [ j ]表示有j个元素,每个元素包含i个值(和其他许多语言不同)
2.二维数组可以直接获取长度,既可以获得所有元素个数,也可以获得单独元素有多少值
3.对二维数组进行增删改等操作时时是与初始化时反过来的,即uint[ i ] [ j ]表示第i个元素的第j个值(和其他许多语言一样)
4.不支持push方法,也不支持对长度进行修改
原文链接:https://blog.csdn.net/weixin_45067603/article/details/105751748

 可变长度二维数组


结论
1.初始化时,uint[ i ] [ j ]表示有j个元素,每个元素包含i个值(和其他许多语言不同)
2.可变长度二维数组可以直接获取长度,既可以获得所有元素个数,也可以获得单独元素有多少值
3.对二维数组进行增删改等操作时时是与初始化时反过来的,即uint[ i ] [ j ]表示第i个元素的第j个值(和其他许多语言一样)
4.不支持push方法
5.支持对长度进行修改,修改后默认值为0
6.未声明的值不能直接赋值,修改长度之后默认有值才可以
 原文链接:https://blog.csdn.net/weixin_45067603/article/details/105751748

数组字面量


结论:
1.返回数组时,returns()括号里面的类型要与return的数据类型相同。
通过getarr1和getarr2可以知道,return之后的会默认最小数据类型,比如小于255的就默认为uint8类型, return [1,2,3]就默认uint8,return [256,2,3]就默认uint16等等,然而returns()里面的uint默认为uint256,所以报错,需注意
2.可以通过对return里面任意一个数值来进行强制转换来改变数据类型
3.可以直接接受参数来进行计算

pragma solidity ^0.4.0;

contract finaltest
    function getarr1() public view returns(uint[3])
    //    return [1,2,3];  报错,此处为uint8,而需要返回的是uint256
    
    function getarr2() public view returns(uint[3])
    //    return [256,2,3];报错,此处为uint16,而需要返回的是uint256
    
    function getarr3() public view returns(uint8[3])
        return [1,2,3];    //成功
    
    function getarr4() public view returns(uint16[3])
        return [256,2,3];  //成功
    
    function getarr5() public view returns(uint32[3])
        return [uint32(1),2,3]; //可以通过对return里面任意一个数值来
                                //进行强制转换来改变数据类型
    
    function getarr6(uint[] num) public view returns(uint)
        uint sum=0;
        for(uint i=0;i<num.length;i++)
            sum+=num[i]; //可以直接接受参数来进行计算,
           //参数需要按照格式来,此处就应该为[x1,x2,x3,x4,...,xn]
        
        return sum;
    

以上是关于Solidity 学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

《solidity学习笔记》chapter 2-solidity基础知识

区块链Solidity智能合约语言学习笔记

solidity学习笔记02-数据存储篇

solidity学习笔记02-数据存储篇

solidity学习笔记01-基础知识

WEB3之路-- solidity学习笔记