当前位置:   article > 正文

solidity_erc20事件

erc20事件

Type:

整数

int public _int =-1

uint public _uint =1

uint256 public _number =20220330 //256位整数

//地址

address(存储20字节) public _address=0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;

address payable public _address1 = payable(_address); // payable address,可以转账、查余额

// 地址类型的成员

uint256 public balance = _address1.balance; // balance of address

定长字节数组(数值类型)

bytes32 public _byte32 ="MiniSolidity";

bytes1 public  _byte=_byte32[0]

不定长字节数组(引用类型)

//枚举(enum)

enum ActionSet {Buy,Hold,Sell}

//创建enum action

ActionSet action=ActionSet.Buy;

//枚举可以显式地和uint相互转换

函数权限关键字

pure:既不能读也不能写入链上状态变量

view:可以读但不能写入

internal 和external

可以通过external类型的函数间接调用internal类型的函数

返回值

命名式返回 public pure returns(uint256 _number, bool _bool, uint256[3] memory _array)

读取所有返回值

uint256 _number;

bool _bool;

uint256[3] memory _array;

(_number, _bool, _array) = returnNamed();

部分返回值

(,_bool,)=returnNamed();

数据位置

  1. storage:合约里的状态变量默认都是storage,存储在链上。

  2. memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。

  3. calldata:和memory类似,存储在内存中,不上链。与memory的不同点在于calldata变量不能修改(immutable),一般用于函数的参数。

  • storage(合约的状态变量)赋值给本地storage(函数里的)时候,会创建引用,改变新变量会影响原变量。

  • 其他情况下,赋值创建的是本体的副本,即对二者之一的修改,并不会同步到另一方

变量的作用域

Solidity中变量按作用域划分有三种,分别是状态变量(state variable),局部变量(local variable)和全局变量(global variable)

状态变量:存储在链上的数据 例如uint public x=1;

局部变量: 局部变量是仅在函数执行过程中有效的变量,函数退出后,变量无效。局部变量的数据存储在内存里,不上链,gas低。局部变量在函数内声明

例如:uint xx=1

全局变量:是全局范围工作的变量,都是solidity预留关键字。他们可以在函数内不声明直接使用:例如:address sender = msg.sender;

以太单位:

Solidity中不存在小数点,以0代替为小数点,来确保交易的精确度,并且防止精度的损失,利用以太单位可以避免误算的问题,方便程序员在合约中处理货币交易。

1 wei=1

1 gwei =1e9=1000000000

1 ether=1e18=10000000000000000000

时间单位:

  • seconds: 1

  • minutes: 60 seconds = 60

  • hours: 60 minutes = 3600

  • days: 24 hours = 86400

  • weeks: 7 days = 604800

数组 array

数组(Array)是solidity常用的一种变量类型,用来存储一组数据(整数,字节,地址等等)。数组分为固定长度数组和可变长度数组两种:

  • 固定长度数组:在声明时指定数组的长度。用T[k]的格式声明,其中T是元素的类型,k是长度,例如:

  • 固定长度 Array

  •    uint[8] array1;

  •    bytes1[5] array2;

  •    address[100] array3;

可变长度数组(动态数组):在声明时不指定数组的长度。用T[]的格式声明,其中T是元素的类型,例如:

可变长度 Array

   uint[] array4;

   bytes1[] array5;

   address[] array6;

  bytes array7;

//例子1

uint[5] arr = [0,1,2,3,4];//创建一个定长的数组

   uint[] storageArr;

   function a() public {

//通过索引查询时要先初始化

       uint[5] memory arr1 = [uint(0),1,2,3,4];//uint8显示的转换为uint256,否则会报类型错误。

       uint[] memory memoryArr;

       //storageArr[0] = 12;

       //memoryArr[0] = 13; //执行会报VM error: invalid opcode.,原因是数组还没有执行初始化。

       storageArr = new uint[](5);

       memoryArr = new uint[](5);

       storageArr[0] = 12;

       memoryArr[0] = 13;

   }

2、存储位置对赋值的影响:

值类型申明不允许指定storage/memory,值类型之间赋值都是创建拷贝。

下面指引用类型赋值操作:

Storage与memory相互赋值,会创建拷贝。

storage/memory同一存储区域内的引用类型(除状态变量)之间赋值不会创建拷贝,创建引用;所有变量给Storage状态变量赋值,都会创建拷贝。

Storage状态赋值给Storage局部变量:不创建拷贝,仅创建一个引用。

Memory引用类型赋值给Memory引用类型:不创建拷贝,仅创建一个引用。

memory变量不能给storage局部变量赋值,但可以给storage状态变量赋值。

bytes比较特殊是数组,但不用写[],声明单字节数组用bytes[]或bytes但是bytes1更省gas

//memory动态数组

uint[] memory array8 =new uint[](5)

bytes memory array9=new bytes(9)

用new操作符来创建,但是必须声明长度,并且声明后长度不能改变。

数组成员

  • length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。

  • push(): 动态数组拥有push()成员,可以在数组最后添加一个0元素,并返回该元素的引用。

  • push(x): 动态数组拥有push(x)成员,可以在数组最后添加一个x元素。

  • pop(): 动态数组拥有pop()成员,可以移除数组最后一个元素。

  • push:

  • 1.面对uint[], push可以加一个uint

  • 2.面对bytes[],push只可以加256以内的(2的8次方)

  • push返回的数组长度

结构体 struct

Solidity支持通过构造结构体的形式定义新的类型。结构体中的元素可以是原始类型,也可以是引用类型;结构体可以作为数组或映射的元素。创建结构体的方法:

struct Student{

uint256 id;

uint256 score;

}

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

//给结构体赋值的四种方法

在函数中创建一个storage的struct引用

function initStudent1() external{

Student storage _student =student //这里面定义的_student会指向student,更改里面的外面也会修改

_student.id=11;

_student.score=100; }

//方法2:直接俄引用状态变量的struct

function initStudent2( ) external{

student.id=1;

student.sscore=80;z

}

//方法3:构造函数

function initStudent3() external{

student =Student(3,90);

}

//方法4:key value

function initStudent4() external{

student=Student({id :4,score:60});

}

//映射Mapping( 哈希表)将数组的数字下标转化为key下标,映射不能用结构体

mapping(uint =>address) public idToAddress ;//id映射到地址

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

映射实质上就是 代币迁移

在旧币钱包和新币钱包之间建立一个对应关系

  • 规则2:映射的存储位置必须是storage,因此可以用于合约的状态变量,函数中的storage变量,和library函数的参数(见例子)。不能用于public函数的参数或返回结果中,因为mapping记录的是一种关系 (key - value pair)。

  • 规则3:如果映射声明为public,那么Solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value。

  • 规则4:给映射新增的键值对的语法为_Var[_Key] = _Value,其中_Var是映射变量名,_Key和_Value对应新增的键值对。

delete操作符

delete a会让变量a的值变为初始值。

// delete操作符

bool public _bool2 = true;

function d() external {

delete _bool2; // delete 会让_bool2变为默认值,false

}

映射的原理

  • 原理1: 映射不储存任何键(Key)的资讯,也没有length的资讯。

  • 原理2: 映射使用keccak256(abi.encodePacked(key, slot))当成offset存取value,其中slot是映射变量定义所在的插槽位置。

  • 原理3: 因为Ethereum会定义所有未使用的空间为0,所以未赋值(Value)的键(Key)初始值都是各个type的默认值,如uint的默认值是0。

constant和immutable

只有数值变量可以声明constant和immutable

状态变量声明这个两个关键字之后,不能在合约后更改数值。这样做的好处是提升合约的安全性并节省gas。

//constant变量必须在声明时或构造函数中初始化

uint256 constant CONSTANT_NUM=10;

string constant CONSTANT_STRING = "0xAA";

bytes constant CONSTANT_BYTES = "WTF";

address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000;

immutable变量可以在声明时或构造函数中初始化,因此更加灵活。

// immutable变量可以在constructor里初始化,之后不能改变

   uint256 public immutable IMMUTABLE_NUM = 9999999999;

   address public immutable IMMUTABLE_ADDRESS;

   uint256 public immutable IMMUTABLE_BLOCK;

   uint256 public immutable IMMUTABLE_TEST;

你可以使用全局变量例如address(this),block.number ,或者自定义的函数给immutable变量初始化。在下面这个例子,我们利用了test()函数给IMMUTABLE_TEST初始化为9:

address public immutable immutable_address;

uint256 public immutable immutable_block;

constructor(){

immutablle_address=address(this);

immutable_block =block.number;

immutable_test=test();

}

function test() public pure returns(uint256){

uint256 what=9;

return(what);

}

构造函数

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

老写法

constructor(){

}

//新写法

address owner /.地址

function 合约名 () public{

owner =msg.sender

}

用solidity实现插入排序

修饰器

修饰器(modifier)是solidity特有的语法,类似于面向对象编程中的decorator,声明函数拥有的特性,并减少代码冗余。它就像钢铁侠的智能盔甲,穿上它的函数会带有某些特定的行为。modifier的主要使用场景是运行函数前的检查,例如地址,变量,余额等。

(类似于vue的过滤器,对数据进行筛选

modifier onlyOwner{

require(msg.sender ==owner);//检查调用者是否为owner,

//require作用是如果值为true则执行下面的语句

_;//如果是的话继续运行函数主体;否则报错并revert交易

}

//判断是否以owner用户登录

//带有onlyOwner修饰符的函数只能owner调用

function changeOwner(address _newOwner)external onlyOwner{ //修改owner,类似于修改密码

owner =_newOwner;

}

事件

Solidity中的事件(event)是EVM上日志的抽象,它具有两个特点:

  • 响应:应用程序(ethers.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。

  • 经济:事件是EVM上比较经济的存储数据的方式,每个大概消耗2,000 gas;相比之下,链上存储一个新变量至少需要20,000 gas。

声明事件

事件的声明由event关键字开头,接着是事件名称,括号里面写好事件需要记录的变量类型和变量名。以ERC20代币合约的Transfer事件为例:

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

我们可以看到,Transfer事件共记录了3个变量from,to和value,分别对应代币的转账地址,接收地址和转账数量,其中from和to前面带有indexed关键字,他们会保存在以太坊虚拟机日志的topics中,方便之后检索。

pragma solidity ^0.8.4;

contract Events {

    // 定义_balances映射变量,记录每个地址的持币数量

    mapping(address => uint256) public _balances;

   

    // 定义Transfer event,记录transfer交易的转账地址,接收地址和转账数量

    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可以为参数创建一个索引, 以便可以通过该参数进行高效的事件过滤和查询。

继承

继承是面向对象编程很重要的组成部分,可以显著减少重复代码。如果把合约看作是对象的话,solidity也是面向对象的编程,也支持继承。

规则

  • virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。

  • override:子合约重写了父合约中的函数,需要加上override关键字。

注意:用override修饰public变量,会重写与变量同名的getter函数

注意:重写方法后,父函数就不再执行。如果想要执行父方法,可以使用 super。

简单继承

我们先写一个简单的爷爷合约Yeye,里面包含1个Log事件和3个function: hip(), pop(), yeye(),输出都是”Yeye”。

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");

   }

}

多重继承

solidity的合约可以继承多个合约。规则:

  1. 继承时要按辈分最高到最低的顺序排。比如我们写一个Erzi合约,继承Yeye合约和Baba合约,那么就要写成contract Erzi is Yeye, Baba,而不能写成contract Erzi is Baba, Yeye,不然就会报错。

  2. 如果某一个函数在多个继承的合约里都存在,比如例子中的hip()和pop(),在子合约里必须重写,不然会报错。

  3. 重写在多个父合约中都重名的函数时,override关键字后面要加上所有父合约名字,例如override(Yeye, Baba)。

contract Erzi is Yeye, Baba{

   // 继承两个function: hip()和pop(),输出值为Erzi。

   function hip() public virtual override(Yeye, Baba){

       emit Log("Erzi");

   }

   function pop() public virtual override(Yeye, Baba) {

       emit Log("Erzi");

   }

修饰器的继承

Solidity中的修饰器(Modifier)同样可以继承,用法与函数继承类似,在相应的地方加virtual和override关键字即可。

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 C is A {

   constructor(uint _c) A(_c * _c) {}

}

调用父合约的函数

  1. 直接调用:子合约可以直接用父合约名.函数名()的方式来调用父合约函数,例如Yeye.pop()。

function callParent() public{

       Yeye.pop();

   }

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

function callParentSuper() public{

       // 将调用最近的父合约函数,Baba.pop()

       super.pop();

   }

钻石继承

在面向对象编程中,钻石继承(菱形继承)指一个派生类同时有两个或两个以上的基类。

在多重+菱形继承链条上使用super关键字时,需要注意的是使用super会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约。

我们先写一个合约God,再写Adam和Eve两个合约继承God合约,最后让创建合约people继承自Adam和Eve,每个合约都有foo和bar两个函数。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

/* 继承树:

 God

/  \

Adam Eve

\  /

people

*/

contract God {

   event Log(string message);

   function foo() public virtual {

       emit Log("God.foo called");

   }

   function bar() public virtual {

       emit Log("God.bar called");

   }

}

contract Adam is God {

   function foo() public virtual override {

       emit Log("Adam.foo called");

   }

   function bar() public virtual override {

       emit Log("Adam.bar called");

       super.bar();

   }

}

contract Eve is God {

   function foo() public virtual override {

       emit Log("Eve.foo called");

       Eve.foo();

   }

   function bar() public virtual override {

       emit Log("Eve.bar called");

       super.bar();

   }

}

contract people is Adam, Eve {

   function foo() public override(Adam, Eve) {

       super.foo();

   }

   function bar() public override(Adam, Eve) {

       super.bar();

   }

这种继承属于菱形继承问题,solidity需要保证继承不会重复,god如果再显示继承,就会继承多次god

重复继承是不符合程序设计的原则。

抽象合约

如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体{}中的内容,则必须将该合约标为abstract,不然编译会报错;另外,未实现的函数需要加virtual,以便子合约重写。拿我们之前的插入排序合约为例,如果我们还没想好具体怎么实现插入排序函数,那么可以把合约标为abstract,之后让别人补写上。

abstract contract InsertionSort{

   function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);

}

接口

接口类似于抽象合约,但它不实现任何功能。接口的规则:

  1. 不能包含状态变量

  2. 不能包含构造函数

  3. 不能继承除接口外的其他合约

  4. 所有函数都必须是external且不能有函数体

  5. 继承接口的非抽象合约必须实现接口定义的所有功能

虽然接口不实现任何功能,但它非常重要。接口是智能合约的骨架,定义了合约的功能以及如何触发它们:如果智能合约实现了某种接口(比如ERC20或ERC721),其他Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息:

  1. 合约里每个函数的bytes4选择器,以及函数签名函数名(每个参数类型)。

  2. 接口id(更多信息见EIP165

另外,接口与合约ABI(Application Binary Interface)等价,可以相互转换:编译接口可以得到合约的ABI,利用abi-to-sol工具也可以将ABI json文件转换为接口sol文件。

我们以ERC721接口合约IERC721为例,它定义了3个event和9个function,所有ERC721标准的NFT都实现了这些函数。我们可以看到,接口和常规合约的区别在于每个函数都以;代替函数体{ }结尾。

IERC721事件

IERC721包含3个事件,其中Transfer和Approval事件在ERC20中也有。

  • Transfer事件:在转账时被释放,记录代币的发出地址from,接收地址to和tokenid。

  • Approval事件:在授权时释放,记录授权地址owner,被授权地址approved和tokenid。

  • ApprovalForAll事件:在批量授权时释放,记录批量授权的发出地址owner,被授权地址operator和授权与否的approved。

IERC721函数

  • balanceOf:返回某地址的NFT持有量balance。

  • ownerOf:返回某tokenId的主人owner。

  • transferFrom:普通转账,参数为转出地址from,接收地址to和tokenId。

  • safeTransferFrom:安全转账(如果接收方是合约地址,会要求实现ERC721Receiver接口)。参数为转出地址from,接收地址to和tokenId。

  • approve:授权另一个地址使用你的NFT。参数为被授权地址approve和tokenId。

  • getApproved:查询tokenId被批准给了哪个地址。

  • setApprovalForAll:将自己持有的该系列NFT批量授权给某个地址operator。

  • isApprovedForAll:查询某地址的NFT是否批量授权给了另一个operator地址。

  • safeTransferFrom:安全转账的重载函数,参数里面包含了data。

什么时候使用接口?

如果我们知道一个合约实现了IERC721接口,我们不需要知道它具体代码实现,就可以与它交互。

无聊猿BAYC属于ERC721代币,实现了IERC721接口的功能。我们不需要知道它的源代码,只需知道它的合约地址,用IERC721接口就可以与它交互,比如用balanceOf()来查询某个地址的BAYC余额,用safeTransferFrom()来转账BAYC。

异常

写智能合约经常会出bug,solidity中的异常命令帮助我们debug。

Error

error是solidity 0.8.4版本新加的内容,方便且高效(省gas)地向用户解释操作失败的原因,同时还可以在抛出异常的同时携带参数,帮助开发者更好地调试。人们可以在contract之外定义异常。下面,我们定义一个TransferNotOwner异常,当用户不是代币owner的时候尝试转账,会抛出错误:

error TransferNotOwner(); // 自定义error

我们也可以定义一个携带参数的异常,来提示尝试转账的账户地址

error TransferNotOwner(address sender); // 自定义的带参数的error

在执行当中,error必须搭配revert(回退)命令使用。

function transferOwner1(uint256 tokenId, address newOwner) public {

        if(_owners[tokenId] != msg.sender){

            revert TransferNotOwner();

            // revert TransferNotOwner(msg.sender);

        }

        _owners[tokenId] = newOwner;

    }

Require

require命令是solidity 0.8版本之前抛出异常的常用方法,目前很多主流合约仍然还在使用它。它很好用,唯一的缺点就是gas随着描述异常的字符串长度增加,比error命令要高。使用方法:require(检查条件,"异常的描述"),当检查条件不成立的时候,就会抛出异常。

我们用require命令重写一下上面的transferOwner函数:

    function transferOwner2(uint256 tokenId, address newOwner) public {

        require(_owners[tokenId] == msg.sender, "Transfer Not Owner");

        _owners[tokenId] = newOwner;

    }

Assert

assert命令一般用于程序员写程序debug,因为它不能解释抛出异常的原因(比require少个字符串)。它的用法很简单,assert(检查条件),当检查条件不成立的时候,就会抛出异常。

我们用assert命令重写一下上面的transferOwner函数:

function transferOwner3(uint256 tokenId, address newOwner) public {

        assert(_owners[tokenId] == msg.sender);

        _owners[tokenId] = newOwner;

    }

重载

solidity中允许函数进行重载(overloading),即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。注意,solidity不允许修饰器(modifier)重载。

函数重载

举个例子,我们可以定义两个都叫saySomething()的函数,一个没有任何参数,输出"Nothing";另一个接收一个string参数,输出这个string。

实参匹配(Argument Matching)

在调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。 如果出现多个匹配的重载函数,则会报错。下面这个例子有两个叫f()的函数,一个参数为uint8,另一个为uint256:

    function f(uint8 _in) public pure returns(uint8 out){

out=_in

}

    function f(uint256 _in)public pure returns(uint256 out){

out=_in;

}

String库合约

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

    }

}

1. 利用using for指令

指令using A for B;可用于附加库合约(从库 A)到任何类型(B)。添加完指令后,库A中的函数会自动添加为B类型变量的成员,可以直接调用。注意:在调用的时候,这个变量会被当作第一个参数传递给函数

2. 通过库合约名称调用函数

// 直接通过库合约名调用

    function getString2(uint256 _number) public pure returns(string memory){

        return Strings.toHexString(_number);

    }

接收ETH函数 receive

receive()函数是在合约收到ETH转账时被调用的函数。一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }。receive()函数不能有任何的参数,不能返回任何值,必须包含external和payable。

当合约接收ETH的时候,receive()会被触发。receive()最好不要执行太多的逻辑因为如果别人用send和transfer方法发送ETH的话,gas会限制在2300,receive()太复杂可能会触发Out of Gas报错;如果用call就可以自定义gas执行更复杂的逻辑(这三种发送ETH的方法我们之后会讲到)。

我们可以在receive()里发送一个event,例如:

    // 定义事件

    event Received(address Sender, uint Value);

    // 接收ETH时释放Received事件

    receive() external payable {

        emit Received(msg.sender, msg.value);

    }

回退函数 fallback

fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract。fallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { ... }。

我们定义一个fallback()函数,被触发时候会释放fallbackCalled事件,并输出msg.sender,msg.value和msg.data:

    event fallbackCalled(address Sender, uint Value, bytes Data);

    // fallback

    fallback() external payable{

        emit fallbackCalled(msg.sender, msg.value, msg.data);

    }

触发fallback() 还是 receive()?

           接收ETH

              |

         msg.data是空?

            /  \

          是    否

          /      \

receive()存在?   fallback()

        / \

       是  否

      /     \

receive()   fallback()

简单来说,合约接收ETH时,msg.data为空且存在receive()时,会触发receive();msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable。

receive()和payable fallback()均不存在的时候,向合约直接发送ETH将会报错(你仍可以通过带有payable的函数向合约发送ETH)。

接收ETH合约

我们先部署一个接收ETH合约ReceiveETH。ReceiveETH合约里有一个事件Log,记录收到的ETH数量和gas剩余。还有两个函数,一个是receive()函数,收到ETH被触发,并发送Log事件;另一个是查询合约ETH余额的getBalance()函数。

contract ReceiveETH {

    // 收到eth事件,记录amount和gas

    event Log(uint amount, uint gas);

    

    // receive方法,接收eth时被触发

    receive() external payable{

        emit Log(msg.value, gasleft());

    }

    

    // 返回合约ETH余额

    function getBalance() view public returns(uint) {

        return address(this).balance;

    }

}

发送ETH合约

我们将实现三种方法向ReceiveETH合约发送ETH。首先,先在发送ETH合约SendETH中实现payable的构造函数和receive(),让我们能够在部署时和部署后向合约转账。

contract SendETH {

    // 构造函数,payable使得部署的时候可以转eth进去

    constructor() payable{}

    // receive方法,接收eth时被触发

    receive() external payable{}

}

transfer

  • 用法是接收方地址.transfer(发送ETH数额)。

  • transfer()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。

  • transfer()如果转账失败,会自动revert(回滚交易)。

代码样例,注意里面的_to填ReceiveETH合约的地址,amount是ETH转账金额:

// 用transfer()发送ETH

function transferETH(address payable _to, uint256 amount) external payable{

    _to.transfer(amount);

}

send

  • 用法是接收方地址.send(发送ETH数额)。

  • send()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。

  • send()如果转账失败,不会revert。

  • send()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下。

代码样例:

// send()发送ETH

function sendETH(address payable _to, uint256 amount) external payable{

    // 处理下send的返回值,如果失败,revert交易并发送error

    bool success = _to.send(amount);

    if(!success){

        revert SendFailed();

    }

}

call

  • 用法是接收方地址.call{value: 发送ETH数额}("")。

  • call()没有gas限制,可以支持对方合约fallback()或receive()函数实现复杂逻辑。

  • call()如果转账失败,不会revert。

  • call()的返回值是(bool, data),其中bool代表着转账成功或失败,需要额外代码处理一下。

代码样例:

// call()发送ETH

function callETH(address payable _to, uint256 amount) external payable{

    // 处理下call的返回值,如果失败,revert交易并发送error

    (bool success,) = _to.call{value: amount}("");

    if(!success){

        revert CallFailed();

    }

}

目标合约

我们先写一个简单的合约OtherContract,用于被其他合约调用。

contract OtherContract {

    uint256 private _x = 0; // 状态变量_x

    // 收到eth的事件,记录amount和gas

    event Log(uint amount, uint gas);

    

    // 返回合约ETH余额

    function getBalance() view public returns(uint) {

        return address(this).balance;

    }

    // 可以调整状态变量_x的函数,并且可以往合约转ETH (payable)

    function setX(uint256 x) external payable{

        _x = x;

        // 如果转入ETH,则释放Log事件

        if(msg.value > 0){

            emit Log(msg.value, gasleft());

        }

    }

    // 读取_x

    function getX() external view returns(uint x){

        x = _x;

    }

}

状态修饰符view

view 在本地运行不消耗gas

view与constant等价,constant在0.5版本之后弃用

call的使用规则

call的使用规则如下:

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

其中字节码利用结构化编码函数abi.encodeWithSignature获得:

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

函数签名为"函数名(逗号分隔的参数类型)"。例如abi.encodeWithSignature("f(uint256,address)", _x, _addr)。

另外call在调用合约时可以指定交易发送的ETH数额和gas:

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

看起来有点复杂,下面我们举个call应用的例子。

// call setX(),同时可以发送ETH

    (bool success, bytes memory data) = _addr.call{value: msg.value}(

        abi.encodeWithSignature("setX(uint256)", x)

    );

3. 调用getX函数

下面我们调用getX()函数,它将返回目标合约_x的值,类型为uint256。我们可以利用abi.decode来解码call的返回值data,并读出数值。

function callGetX(address _addr) external returns(uint256){

// call getX()

(bool success, bytes memory data) = _addr.call(

abi.encodeWithSignature("getX()")

);

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

return abi.decode(data, (uint256));

}

从Response事件的输出,我们可以看到data为0x0000000000000000000000000000000000000000000000000000000000000005。而经过abi.decode,最终返回值为5。

4. 调用不存在的函数

如果我们给call输入的函数不存在于目标合约,那么目标合约的fallback函数会被触发。

function callNonExist(address _addr) external{

// call 不存在的函数

(bool success, bytes memory data) = _addr.call(

abi.encodeWithSignature("foo(uint256)")

);

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

}

上面例子中,我们call了不存在的foo函数。call仍能执行成功,并返回success,但其实调用的目标合约fallback函数。

delegatecall

  1. 代理合约(Proxy Contract):将智能合约的存储合约和逻辑合约分开:代理合约(Proxy Contract)存储所有相关的变量,并且保存逻辑合约的地址;所有函数存在逻辑合约(Logic Contract)里,通过delegatecall执行。当升级时,只需要将代理合约指向新的逻辑合约即可。

  2. EIP-2535 Diamonds(钻石):钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石是具有多个实施合约的代理合约。 更多信息请查看:钻石标准简介

delegatecall例子

调用结构:你(A)通过合约B调用目标合约C。

被调用的合约C

我们先写一个简单的目标合约C:有两个public变量:num和sender,分别是uint256和address类型;有一个函数,可以将num设定为传入的_num,并且将sender设为msg.sender。

// 被调用的合约C

contract C {

uint public num;

address public sender;

function setVars(uint _num) public payable {

num = _num;

sender = msg.sender;

}

}

发起调用的合约B

首先,合约B必须和目标合约C的变量存储布局必须相同,两个变量,并且顺序为num和sender

contract B {

uint public num;

address public sender;

接下来,我们分别用call和delegatecall来调用合约C的setVars函数,更好的理解它们的区别。

callSetVars函数通过call来调用setVars。它有两个参数_addr和_num,分别对应合约C的地址和setVars的参数。

// 通过call来调用C的setVars()函数,将改变合约C里的状态变量

function callSetVars(address _addr, uint _num) external payable{

// call setVars()

(bool success, bytes memory data) = _addr.call(

abi.encodeWithSignature("setVars(uint256)", _num)

);

}

而delegatecallSetVars函数通过delegatecall来调用setVars。与上面的callSetVars函数相同,有两个参数_addr和_num,分别对应合约C的地址和setVars的参数。

// 通过delegatecall来调用C的setVars()函数,将改变合约B里的状态变量

function delegatecallSetVars(address _addr, uint _num) external payable{

// delegatecall setVars()

(bool success, bytes memory data) = _addr.delegatecall(

abi.encodeWithSignature("setVars(uint256)", _num)

);

}

}

在以太坊链上,用户(外部账户,EOA)可以创建智能合约,智能合约同样也可以创建新的智能合约。去中心化交易所uniswap就是利用工厂合约(PairFactory)创建了无数个币对合约(Pair)。这一讲,我会用简化版的uniswap讲如何通过合约创建合约。

create

有两种方法可以在合约中创建新合约,create和create2,这里我们讲create,下一讲会介绍create2。

create的用法很简单,就是new一个合约,并传入新合约构造函数所需的参数:

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

其中Contract是要创建的合约名,x是合约对象(地址),如果构造函数是payable,可以创建时转入_value数量的ETH,params是新合约构造函数的参数。

极简Uniswap

Uniswap V2核心合约中包含两个合约:

  1. UniswapV2Pair: 币对合约,用于管理币对地址、流动性、买卖。

  2. UniswapV2Factory: 工厂合约,用于创建新的币对,并管理币对地址。

下面我们用create方法实现一个极简版的Uniswap:Pair币对合约负责管理币对地址,PairFactory工厂合约用于创建新的币对,并管理币对地址。

Pair合约

contract Pair{

address public factory; // 工厂合约地址

address public token0; // 代币1

address public token1; // 代币2

constructor() payable {

factory = msg.sender;

}

// called once by the factory at time of deployment

function initialize(address _token0, address _token1) external {

require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check

token0 = _token0;

token1 = _token1;

}

}

Pair合约很简单,包含3个状态变量:factory,token0和token1。

构造函数constructor在部署时将factory赋值为工厂合约地址。initialize函数会由工厂合约在部署完成后手动调用以初始化代币地址,将token0和token1更新为币对中两种代币的地址。

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

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

PairFactory

contract PairFactory{

mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址

address[] public allPairs; // 保存所有Pair地址

function createPair(address tokenA, address tokenB) external returns (address pairAddr) {

// 创建新合约

Pair pair = new Pair();

// 调用新合约的initialize方法

pair.initialize(tokenA, tokenB);

// 更新地址map

pairAddr = address(pair);

allPairs.push(pairAddr);

getPair[tokenA][tokenB] = pairAddr;

getPair[tokenB][tokenA] = pairAddr;

}

}

工厂合约(PairFactory)有两个状态变量getPair是两个代币地址到币对地址的map,方便根据代币找到币对地址;allPairs是币对地址的数组,存储了所有代币地址。

PairFactory合约只有一个createPair函数,根据输入的两个代币地址tokenA和tokenB来创建新的Pair合约。

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号