赞
踩
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();
数据位置
storage:合约里的状态变量默认都是storage,存储在链上。
memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。
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)是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返回的数组长度
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 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
状态变量声明这个两个关键字之后,不能在合约后更改数值。这样做的好处是提升合约的安全性并节省gas。
uint256 constant CONSTANT_NUM=10;
string constant CONSTANT_STRING = "0xAA";
bytes constant CONSTANT_BYTES = "WTF";
address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000;
// 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的合约可以继承多个合约。规则:
继承时要按辈分最高到最低的顺序排。比如我们写一个Erzi合约,继承Yeye合约和Baba合约,那么就要写成contract Erzi is Yeye, Baba,而不能写成contract Erzi is Baba, Yeye,不然就会报错。
如果某一个函数在多个继承的合约里都存在,比如例子中的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");
}
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) {}
}
调用父合约的函数
直接调用:子合约可以直接用父合约名.函数名()的方式来调用父合约函数,例如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);
}
接口类似于抽象合约,但它不实现任何功能。接口的规则:
不能包含状态变量
不能包含构造函数
不能继承除接口外的其他合约
所有函数都必须是external且不能有函数体
继承接口的非抽象合约必须实现接口定义的所有功能
虽然接口不实现任何功能,但它非常重要。接口是智能合约的骨架,定义了合约的功能以及如何触发它们:如果智能合约实现了某种接口(比如ERC20或ERC721),其他Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息:
合约里每个函数的bytes4选择器,以及函数签名函数名(每个参数类型)。
接口id(更多信息见EIP165)
另外,接口与合约ABI(Application Binary Interface)等价,可以相互转换:编译接口可以得到合约的ABI,利用abi-to-sol工具也可以将ABI json文件转换为接口sol文件。
我们以ERC721接口合约IERC721为例,它定义了3个event和9个function,所有ERC721标准的NFT都实现了这些函数。我们可以看到,接口和常规合约的区别在于每个函数都以;代替函数体{ }结尾。
IERC721包含3个事件,其中Transfer和Approval事件在ERC20中也有。
Transfer事件:在转账时被释放,记录代币的发出地址from,接收地址to和tokenid。
Approval事件:在授权时释放,记录授权地址owner,被授权地址approved和tokenid。
ApprovalForAll事件:在批量授权时释放,记录批量授权的发出地址owner,被授权地址operator和授权与否的approved。
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是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命令是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命令一般用于程序员写程序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。
在调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。 如果出现多个匹配的重载函数,则会报错。下面这个例子有两个叫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库合约是将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
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);
}
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()函数会在调用合约不存在的函数时被触发。可用于接收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合约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;
}
}
我们将实现三种方法向ReceiveETH合约发送ETH。首先,先在发送ETH合约SendETH中实现payable的构造函数和receive(),让我们能够在部署时和部署后向合约转账。
contract SendETH {
// 构造函数,payable使得部署的时候可以转eth进去
constructor() payable{}
// receive方法,接收eth时被触发
receive() external payable{}
}
用法是接收方地址.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(发送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{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(字节码);
其中字节码利用结构化编码函数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
代理合约(Proxy Contract):将智能合约的存储合约和逻辑合约分开:代理合约(Proxy Contract)存储所有相关的变量,并且保存逻辑合约的地址;所有函数存在逻辑合约(Logic Contract)里,通过delegatecall执行。当升级时,只需要将代理合约指向新的逻辑合约即可。
EIP-2535 Diamonds(钻石):钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石是具有多个实施合约的代理合约。 更多信息请查看:钻石标准简介。
调用结构:你(A)通过合约B调用目标合约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必须和目标合约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和create2,这里我们讲create,下一讲会介绍create2。
create的用法很简单,就是new一个合约,并传入新合约构造函数所需的参数:
Contract x = new Contract{value: _value}(params)
其中Contract是要创建的合约名,x是合约对象(地址),如果构造函数是payable,可以创建时转入_value数量的ETH,params是新合约构造函数的参数。
Uniswap V2核心合约中包含两个合约:
UniswapV2Pair: 币对合约,用于管理币对地址、流动性、买卖。
UniswapV2Factory: 工厂合约,用于创建新的币对,并管理币对地址。
下面我们用create方法实现一个极简版的Uniswap:Pair币对合约负责管理币对地址,PairFactory工厂合约用于创建新的币对,并管理币对地址。
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讲。
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合约。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。