智能合约-solidity

发布时间 2023-09-27 09:42:45作者: 陌上花i

智能合约-solidity语言学习

Solidity是一种静态类型语言,这意味着每个变量(状态变量和局部变量)都需要在编译时指

定变量的类型。

Solidity 提供了几种基本类型,并且基本类型可以用来组合出复杂类型。

除此之外,类型之间可以在包含运算符号的表达式中进行交互。 关于各种运算符号,可以参考 运算符优先级

“undefined”或“null”值的概念在Solidity中不存在,但是新声明的变量总是有一个 默认值 ,具体的默认值跟类型相关。 要处理任何意外的值,应该使用 错误处理 来恢复整个交易,或者返回一个带有第二个 bool 值的元组表示成功。

1.HelloWeb3(三行代码)

注意写合约脚本时,要规范框架:

// SPDX-License-Identifier: MIT  
pragma solidity ^0.8.4;           
contract HelloWeb3{
    string public _string = "Hello Web3!";
}

每行代码的含义:

1:介绍软件许可

2:声明源文件所用的solidity版本,只能编译 [0.8.4 , 0.9.0) 版本

3-4:定义一个合约。第3行创建合约(contract),并声明合约的名字 HelloWeb3。第4行是合约的内容,我们声明了一个string(字符串)变量_string,并给他赋值 “Hello Web3!”。

值类型

Types

以下类型也称为值类型,因为这些类型的变量将始终按值来传递。 也就是说,当这些变量被用作函数参数或者用在赋值语句中时,总会进行值拷贝。

布尔类型

:可能的取值为字面常量值 truefalse

    // 布尔运算
    bool public _bool1 = !_bool; //取非
    bool public _bool2 = _bool && _bool1; //与
    bool public _bool3 = _bool || _bool1; //或
    bool public _bool4 = _bool == _bool1; //相等
    bool public _bool5 = _bool != _bool1; //不相等

整型

uint , int int / uint :分别表示有符号和无符号的不同位数的整型变量。 支持关键字 uint8uint256 (无符号,从 8 位到 256 位)以及 int8int256,以 8 位为步长递增。 uintint 分别是 uint256int256 的别名

对于整形 X,可以使用 type(X).mintype(X).max 去获取这个类型的最小值与最大值

    // 整数运算
    uint256 public _number1 = _number + 1; // +,-,*,/
    uint256 public _number2 = 2**2; // 指数
    uint256 public _number3 = 7 % 2; // 取余数
    bool public _numberbool = _number2 > _number3; // 比大小

地址类型

地址类型有两种形式,他们大致相同:

  • address:保存一个20字节的值(以太坊地址的大小)。
  • address payable :可支付地址,与 address 相同,不过有成员函数 transfersend

这种区别背后的思想是 address payable 可以向其发送以太币,而不能先一个普通的 address 发送以太币,例如,它可能是一个智能合约地址,并且不支持接收以太币。

类型转换:

允许从 address payableaddress 的隐式转换,而从 addressaddress payable 必须显示的转换, 通过 payable(<address>) 进行转换。

address 允许和 uint160、 整型字面常量、bytes20 及合约类型相互转换。

​ 只能通过 payable(...) 表达式把 address 类型和合约类型转换为 address payable。 只有能接 收以太币的合约类型,才能够进行此转换。例如合约要么有 receive 或可支付的回退函数。 注意 payable(0) 是有效的,这是此规则的例外。

  • 地址类型成员变量

    查看所有的成员,可参考 地址成员

    • balancetransfer 成员

    可以使用 balance 属性来查询一个地址的余额, 也可以使用 transfer 函数向一个可支付地址(payable address)发送 以太币Ether (以 wei 为单位):

    address x = 0x123;
    address myAddress = this;
    if (x.balance < 10 && myAddress.balance >= 10) 
               x.transfer(10);
    

    如果当前合约的余额不够多,则 transfer 函数会执行失败,或者如果以太转移被接收帐户拒绝, transfer 函数同样会失败而进行回退

    // 地址定义
    address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
    address payable public _address1 = payable(_address); 
    
    // payable address,可以转账、查余额
    // balance
    uint256 public balance = _address1.balance; 
    // transfer
    address payable addr;
    addr.transfer(1);//合约向addr转账1wei
    
    address x = 0x123;
    address myAddress = this;
    if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);// 向 x 转账 10?

定长字节数组

  • 一个字节占两位,字节乘2是位数(16进制)

  • 字节数组bytes分两种,一种定长(byte, bytes8, bytes32),另一种不定长。不定长的使用频率低,暂时先略过。

  • 定长bytes可以存一些数据,消耗gas比较少。

        // 固定长度的字节数组
        bytes32 public _byte32 = "MiniSolidity"; 
        bytes1 public _byte = _byte32[0]; 
    
  • MiniSolidity 字符串以字节的方式存储进数组_byte32 ,转换成16进制为:

0x4d696e69536f6c69646974790000000000000000000000000000000000000000

  • 0x是象征意义,表示十六进制,四位二进制组成一位十六进制

  • 一个字节占 8 bit,也就是两位十六进制

  • 例如 bytes32 类型有64个16进制位。

  • _byte变量存储_byte32数组的第一个字节,即0x4d (0x是象征意义,代表十六进制)。

// SPDX-License-Identifier: SimPL-2.0
pragma solidity ^0.8.7; 

contract SimpleStorage{

// Solidity是一种静态类型语言,这意味着每个变量(状态变量和局部变量)都需要在编译时指定变量的类型
// 基础数据类型 : bool,uint,int,address,bytes
// 布尔类型
    bool istrue;
// 整型:有符号和无符号
    uint storeData;
    int a;
    int b;
// 地址类型 address,address payable(可支付地址,与address相同,不过有成员transfer和send)
// 可进行相关的类型转换
    address myauont;
    address payable youauont;

// 地址类型可以理解为封装了一些常用方法的类。
// 地址类型成员变量 :详细的可以查看文档

// <address>.balance (uint256) 以 Wei 为单位的地址类型 Address 的余额。
// <address>.codehash (bytes32) 地址类型 Address 的codehash
// <address payable>.transfer(uint256 amount)
// 向 地址类型 Address 发送数量为 amount 的 Wei,失败时抛出异常,使用固定(不可调节)的 2300 gas 的矿工费。
// <address payable>.send(uint256 amount) returns (bool)
// 向 地址类型 Address 发送数量为 amount 的 Wei,失败时返回 false,发送 2300 gas 的矿工费用,不可调节。

//   可以使用 balance 属性来查询一个地址的余额, 也可以使用 transfer 函数向一个可支付地址(payable address)发送 以太币Ether (以 wei 为单位)
// if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);


    function set(uint x) public {
        storeData = x;
    }
    // view、pure等这些参数是函数的修饰符,view 原名 contant
    // 可以将函数声明为view类型,这种情况下要保证不修改状态。
    //函数可以声明为 pure ,在这种情况下,承诺不读取也不修改状态变量。
    function get() public view returns (uint){
        return storeData;
    }
    function f (uint x) public pure returns (uint) {
        return x + 1;
    }
    // 函数重载
 
    // 还有一些特别的函数:
}

函数

Functions

函数定义

函数 function、函数名、(传入参数)、可见性、gas 节省支付性、returns (传出参数)

函数主体{…}前面的开头内容与属性:

    function   function name(types name)   internal|external|public|private   [pure|view|payable]   [returns (types name)]{
    [return] 
    }

中括号部分可省略不写,顺序依次为:function、函数名、(传入参数)、可见性、gas 节省支付性、returns (传出参数)

1.function
声明函数时的固定用法,想写函数,就要以function关键字开头。

2.function name
函数名。

3.(types name)
圆括号里写输入到函数的现实变量类型和名字。

4.internal|external|public|private
函数可见性说明符,一共4种。没标明函数类型的,默认internal。

  • public: 内部外部均可见。(也可用于修饰状态变量,public变量会自动生成 getter函数,用于查询数值).

  • private: 只能从本合约内部访问,继承的合约也不能用(也可用于修饰状态变量)。

  • external: 只能从合约外部访问(但是可以用this.函数名()来调用

  • internal: 只能从合约内部访问,继承的合约可以用(也可用于修饰状态变量)。

5.pure|view|payable
决定函数权限/功能的关键字。payable(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入ETH。

6.returns (types name)
函数返回的变量类型和名称。

函数修饰符

有决定函数何时和被谁调用的可见性修饰符,

private(只能被合约内部调用);

internal(就像private,但是它也能被继承的合约调用);

external(只能从合约外部调用);

public(可以在任何地方调用,无论内部还是外部)。

还有状态修饰符,告诉我们函数如何和区块链交互,view(我们运行这个函数不会更改和保存任何数据);pure(我们的函数不但不会往区块链写数据,它甚至不从区块链读取数据)。这两种修饰符在被从合约外部调用的时候都不话费任何gas(但是它们在被内部其他函数调用的时候会耗费gas)

internal 与external

    // internal: 内部
    function minus() internal {
        number = number - 1;
    }

    // 合约内的函数可以调用内部函数
    function minusCall() external {
        minus();
    }

pure 与 view [gas 节省支付性]

  1. pure:不能读取也不能写入链上变量

  2. view:只能读取但不能写入链上变量

  3. 不写 pure 或 view:可读可写

solidity在函数中加入这两个关键字,是为了节省智能合约中特有的gas fee。合约的状态变量存储在链上所需要的gas fee很贵,如果不改变链上状态,就不用付gas。包含pure跟view关键字的函数是不改写链上状态的,因此用户直接调用他们是不需要付gas的(合约中非pure/view函数调用它们则会改写链上状态,需要付gas)

 
 contract SimpleStorage {
 uint256  public  favoriteNumber;  
   function retrieve() public view returns (uint256){
        favoriteNumber = favoriteNumber+1; ×
        return favoriteNumber;
    }
   }
    这里就错误了,因为是view修饰的函数,因此这个函数中就不能更改和保存合约里的数据,view 是个纯函数,表示视图,不需要花费gas,因为我们不是修改状态,也就是其按钮是蓝色的原因。只call但不发送交易。
    
      function retrieve() public pure  returns (uint256){
        return favoriteNumber;  ×
        return 7;
    }
    
    这里就错误了,它甚至不从区块链读取数据 ,从存储的地方获取。

在以太坊中,以下语句被视为修改链上状态:

  1. 写入状态变量。
  2. 释放事件。
  3. 创建其他合同。
  4. 使用selfdestruct.
  5. 通过调用发送以太币。
  6. 调用任何未标记viewpure的函数。
  7. 使用低级调用(low-level calls)。
  8. 使用包含某些操作码的内联汇编。

如上图所示,公主代表合约中的状态变量(存储在链上),上层为公主,下层人物从左往右对公主的权限依次提高,分别为纯纯牛马、看客、默认 boss

  • pure,中文意思是“纯”,在solidity里理解为“纯纯牛马”。包含pure关键字的函数,不能读取也不能写入存储在链上的状态变量。就像小怪一样,看不到也摸不到公主。
  • view,“看”,在solidity里理解为“看客”。包含view关键字的函数,能读取但也不能写入状态变量。类似马里奥,能看到碧池,但终究是看客,不能入洞房。
  • 不写pure也不写view,函数既可以读取也可以写入状态变量。类似马里奥里的默认boss,可以对公主为所欲为…

例如我们在合约里定义一个状态变量 number = 5

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.4;
    contract FunctionTypes{
        uint256 public number = 5;

定义一个add()函数,每次调用,每次给number + 1

    // 默认
    function add() external{
        number = number + 1;
    }

如果add()包含了pure关键字,例如 function add() pure external,就会报错。因为pure(纯纯牛马)是不配读取合约里的状态变量的,更不配改写。

那pure函数能做些什么?举个例子,你可以给函数传递一个参数 _number,然后让他返回 _number+1。

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

如果add()包含view,比如function add() view external,也会报错。因为view能读取,但不能够改写状态变量。可以稍微改写下方程,让他不改写number,而是返回一个新的变量。

    // view: 看客
    function addView() external view returns(uint256 new_number) {
        new_number = number + 1;
    }

上述+1的函数例子中,pure 需要在函数定义时传入需要的参数, view 则不需要,可以直接读取已有的参数。不过两者都不能改变链上已有的参数。

payable [gas 节省支付性]

    // payable: 递钱,能给合约支付eth的函数
    function minusPayable() external payable returns(uint256 balance) {
        minus(); //-1   
        balance = address(this).balance;
    }

我们定义一个external payableminusPayable()函数,间接的调用minus(),并且返回合约里的ETH余额(this关键字可以让我们引用当前合约的地址)。 我们可以在调用minusPayable()时,往合约里转入1个ETH

我们可以在返回的信息中看到,合约的余额是1 ETH

函数输出

主要还是用命名式返回,直接 returns(参数类型 参数名)

Solidity有两个关键字与函数输出相关:returnreturns

Solidity函数输出主要包括:类型返回、命名式返回、解构式赋值。

返回值 return和returns

他们的区别在于:

  • returns加在函数名后面,用于声明返回的变量类型及变量名;

  • return用于函数主体中,返回指定的变量。

类型返回

    // 类型返回
    function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
            return(1, true, [uint256(1),2,5]);
        }

上面这段代码中,我们声明了returnMultiple()函数将有多个输出:returns(uint256, bool, uint256[3] memory),接着我们在函数主体中用return(1, true, [uint256(1),2,5])确定了返回值。

命名式返回

我们可以在returns中标明返回变量的名称,这样solidity会自动给这些变量初始化,并且自动返回这些函数的值,不需要加return

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

在上面的代码中,我们用returns(uint256 _number, bool _bool, uint256[3] memory _array)声明了返回变量类型以及变量名。这样,我们在主体中只需要给变量_number,_bool和_array赋值就可以自动返回了。

当然,你也可以在命名式返回中用return来返回变量:

// 命名式返回,依然支持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();
  • 读取部分返回值:声明要读取的返回值对应的变量,不读取的留空。下面这段代码中,我们只读取_bool,而不读取返回的_number_array

     ``` solidity
             (, _bool2, ) = returnNamed();
     ```
    

数组和结构体

Arrays &Structs

数组

 //数组和其他语言中的一样
 uint256[] listOfFavoriteNumbers ; // [77,78,90] 与其他编程语言数组一样
 

结构体

  // 定义结构体,相当于构建自己的类型,类似于类
    struct Person{
        string name;
        uint256 favoriteNumber;

    }
    Person[] person;
    直接赋值
    Person public  pat  = Person("pat",7);
    或者按属性名赋值
    Person public  pat  = Person({name:"pat",favoriteNumber:7});
    

数组的添加,跟其他语言数组的操作一样

    // 数组的添加

    function addPerson(string memory _name,uint256 _favoriteNumber) public {
       Person memory newPerson = Person(_name,_favoriteNumber);
       listOfPeople.push(newPerson);
       listOfPeople.push(Person(_name,_favoriteNumber));
    }
       // 删除人 ,是从数组最后开始删除
    function deletePerson() public {
        listOfPeople.pop();
    }
   

错误和警告

Errors & Warnings

遇到Errors & Warnings,一些搜索平台

Resources For This Course

  • AI Frens
    • ChatGPT
      • Just know that it will often get things wrong, but it's very fast!
    • Phind
      • Like ChatGPT, but it searches the web
    • Bard
    • Other AI extensions
  • Github Discussions
    • Ask questions and chat about the course here!
  • Stack Exchange Ethereum
    • Great place for asking technical questions about Ethereum
  • Peeranha
    • Decentralized Stack Exchange!

变量存储属性

5. 变量存储属性(storage/memory/calldata) 与作用域分类(状态变量、局部变量、全局变量)_海阔平的博客-CSDN博客

Memory,Storage,Calldata(Intro)

6个地方存储数据

EVM can access and store information in six places:

  1. Stack
  2. Memory
  3. Storage
  4. Calldata
  5. Code
  6. Logs

solidity中 calldata、memory、storage

目前,引用类型包括struct(结构体)、array(数组)和mapping(映射),使用引用类型必须明确地提供存储该类型的数据位置:

  memory(生存期存在于function(函数)内,超过作用域即失效);

  storage(生存期同contract(合约)一致,状态变量强制为storage);

  calldata(不可修改、非持久的函数参数存储区域,用于存储函数参数,只读,不会永久存储一个数据位置,external function(外部函数)的传入参数(不包括返回参数)强制为calldata,效果类似memory);

变量存储属性(storage/memory/calldata) 与作用域分类(状态变量、局部变量、全局变量)

Solidity中的引用类型
引用类型(Reference Type):包括数组(array),结构体(struct)和映射(mapping),这类变量占空间大,赋值时候直接传递地址(类似指针)。由于这类变量比较复杂,占用存储空间大,我们在使用时必须要声明数据存储的位置。

数据存储位置

  • storage 是默认属性,存在链上消耗gas多

  • memory和calldata存在内存消耗gas少

    • memory 的变量可修改

    • calldata的变量不可修改

solidity数据存储位置有三类:storage,memory和calldata。不同存储位置的gas成本不同。storage类型的数据存在链上,类似计算机的硬盘,消耗gas多;memory和calldata类型的临时存在内存里,消耗gas少。大致用法:

storage:默认属性,合约里的状态变量默认都是storage,存储在链上。
memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。
calldata:和memory类似,存储在内存中,不上链。与memory的不同点在于calldata变量不能修改(immutable),一般用于函数的参数。

memory 暂时存在,临时变量

    // memory 
    function addPerson(string memory _name,uint256 _favoriteNumber) public {
       _name = "cat";   //正确的,可以更改
       listOfPeople.push(Person(_name,_favoriteNumber));
    }

calldata 不能修改它

   function addPerson(string calldata _name,uint256 _favoriteNumber) public {
       _name = "cat";
       listOfPeople.push(Person(_name,_favoriteNumber));
    }



TypeError: Type literal_string "cat" is not implicitly convertible to expected type string calldata.
  --> Simple.sol:43:16:
   |
43 |        _name = "cat";
   |                ^^^^^

映射

Mappings

类似于字典

   //定义mapping
   mapping(string => uint256) public nameToNumber;
  
   function addPerson(string memory _name,uint256 _favoriteNumber) public {
       listOfPeople.push(Person(_name,_favoriteNumber));
       nameToNumber[_name] = _favoriteNumber;  //类似于字典
    }

测试部署

测试部署合约到测试网Sepolia

连接MetaMask:

用网页版,不要用本地编译环境。

EVM

类似于JVM之于Java

EVM实际上是一个标准:如何编译和如何部署智能合约到区块链。