timezone |
---|
Asia/Shanghai |
- 自我介绍
大家好,我是bayazi。开始入门区块链。
- 你认为你会完成本次残酷学习吗?
会完成的
完成了1~10
Solidity中的变量类型:
1.值类型(Value Type)
2.引用类型(Reference Type)
3.映射类型(Mapping Type):
函数:
public:内部和外部均可见。
private:只能从本合约内部访问,继承的合约也不能使用。
external:只能从合约外部访问(但内部可以通过 this.f() 来调用,f是函数名)。
internal: 只能从合约内部访问,继承的合约可以用。
注意 1:合约中定义的函数需要明确指定可见性,它们没有默认值。
注意 2:public|private|internal 也可用于修饰状态变量。public变量会自动生成同名的getter函数,用于查询数值。未标明可见性类型的状态变量,默认为internal。
[pure|view|payable]:决定函数权限/功能的关键字
包含 pure 和 view 关键字的函数是不改写链上状态的,因此用户直接调用它们是不需要付 gas 的(注意,合约中非 pure/view 函数调用 pure/view 函数时需要付gas)。
pure:既不能读取也不能写入链上的状态变量
view:能读取但也不能写入状态变量
returns:跟在函数名后面,用于声明返回的变量类型及变量名。
return:用于函数主体中,返回指定的变量。
可以在 returns 中标明返回变量的名称。Solidity 会初始化这些变量,并且自动返回这些函数的值
存储位置:
Solidity数据存储位置有三类:storage,memory和calldata。storage类型的数据存在链上,类似计算机的硬盘,消耗gas多;memory和calldata类型的临时存在内存里,消耗gas少
1.storage:合约里的状态变量默认都是storage,存储在链上。
2.memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。尤其是如果返回数据类型是变长的情况下,必须加memory修饰,例如:string, bytes, array和自定义结构。
3.calldata:不可变的memory
变量的作用域:
1.状态变量:在合约内、函数外声明,存储在链上
2.局部变量:函数执行过程中有效的变量,在内存中
3.全局变量:全局变量是全局范围工作的变量,都是solidity预留关键字。他们可以在函数内不声明直接使用
(一个文件内好像只能部署最先的合约?,05运行的时候只显示了DataStorage)
数组array
固定长度数组:T[k]
可变长度数组(动态数组):T[]
bytes比较特殊,是数组,但是不用加[]。另外,不能用byte[]声明单字节数组,可以使用bytes或bytes1[]。bytes 比 bytes1[] 省gas。
数组成员
length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。
push(): 动态数组拥有push()成员,可以在数组最后添加一个0元素,并返回该元素的引用。
push(x): 动态数组拥有push(x)成员,可以在数组最后添加一个x元素。
pop(): 动态数组拥有pop()成员,可以移除数组最后一个元素。
映射Mapping
声明映射的格式为mapping(_KeyType => _ValueType)
_KeyType只能选择Solidity内置的值类型,_ValueType可以使用自定义的类型
映射的存储位置必须是storage
如果映射声明为public,那么Solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value
给映射新增的键值对的语法为_Var[_Key] = _Value
delete a会让变量a的值变为初始值。
插入排序
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract Sort {
function sort(uint[] memory a) public pure returns(uint[] memory){
for(uint i = 1; i < a.length; i++){
uint j = i;
while (j > 0) {
if (a[j-1] > a[j]){
uint temp = a[j - 1];
a[j - 1] = a[j];
a[j] = temp;
}
else {
break;
}
j--;
}
}
return(a);
}
}
构造函数
constructor每个合约可以定义一个,并在部署合约的时候自动运行一次
修饰器
声明函数拥有的特性,并减少代码冗余
事件
是EVM
上日志的抽象,它具有两个特点:
响应:应用程序ethers.js可以通过RPC
接口订阅和监听这些事件,并在前端做响应。
经济:事件是EVM
上比较经济的存储数据的方式,每个大概消耗2,000 gas
;相比之下,链上存储一个新变量至少需要20,000 gas
。
声明事件
事件的声明由event
关键字开头,接着是事件名称,括号里面写好事件需要记录的变量类型和变量名
释放事件
我们可以在函数里释放事件。在下面的例子中,每次用_transfer()
函数进行转账操作的时候,都会释放Transfer
事件,并记录相应的变量
EVM日志
每条日志记录都包含主题topics
和数据data
两部分。
主题:用于描述事件,长度不能超过4
。它的第一个元素是事件的签名(哈希)
数据:事件中不带 indexed
的参数会被存储在 data
部分中
继承
virtual
: 父合约中的函数,如果希望子合约重写,需要加上virtual
关键字。
override
:子合约重写了父合约中的函数,需要加上override
关键字。
多重继承
1.继承时要按辈分最高到最低的顺序排
2.如果某一个函数在多个继承的合约里都存在,比如例子中的hip()
和pop()
,在子合约里必须重写,不然会报错。
3.重写在多个父合约中都重名的函数时,override
关键字后面要加上所有父合约名字,例如override(Yeye, Baba)
。
修饰器继承
Solidity
中的修饰器(Modifier
)同样可以继承,用法与函数继承类似,在相应的地方加virtual
和override
关键字即可。
构造函数继承
1.在继承时声明父构造函数的参数
2.在子合约的构造函数中声明构造函数的参数
调用父合约的函数
1.直接调用
2.super调用最近父合约函数
钻石继承
指一个派生类同时有两个或两个以上的基类
在多重+菱形继承链条上使用super
关键字时,需要注意的是使用super
会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约。
抽象合约
如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体{}
中的内容,则必须将该合约标为abstract
,不然编译会报错;另外,未实现的函数需要加virtual
,以便子合约重写。
接口
接口类似于抽象合约,但它不实现任何功能。接口的规则:
1.不能包含状态变量
2.不能包含构造函数
3.不能继承除接口外的其他合约
4.所有函数都必须是external且不能有函数体
5.继承接口的非抽象合约必须实现接口定义的所有功能
接口提供了两个重要的信息:
1.合约里每个函数的bytes4
选择器,以及函数签名函数名(每个参数类型)
。
2.接口id
异常
1.error:方便且高效(省gas
)
error必须搭配revert
2.require:gas
随着描述异常的字符串长度增加
3.assert:程序员写程序debug
重载
不允许修饰器(modifier
)重载
实参匹配
如果出现多个匹配的重载函数,则会报错
库合约
库合约是一系列的函数合集
1.不能存在状态变量
2.不能够继承或被继承
3不能接收以太币
4.不可以被销毁
import
语句可以帮助我们在一个文件中引用另一个文件的内容
准备明天的预推免摸了
Solidity
支持两种特殊的回调函数,receive()
和fallback()
1.接收ETH
2.处理合约中不存在的函数调用(代理合约proxy contract)
接收ETH函数 receive
receive()
函数是在合约收到ETH
转账时被调用的函数。一个合约最多有一个receive()
函数
回退函数 fallback
fallback()
函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract
receive和fallback的区别
合约接收ETH
时,msg.data
为空且存在receive()
时,会触发receive()
;msg.data
不为空或不存在receive()
时,会触发fallback()
,此时fallback()
必须为payable
。均不存在报错
Solidity
有三种方法向其他合约发送ETH
,他们是:transfer()
,send()
和call()
,其中call()
是被鼓励的用法。
transfer:
1.用法是接收方地址.transfer(发送ETH数额)
。
2.transfer()
的gas
限制是2300
,足够用于转账,但对方合约的fallback()
或receive()
函数不能实现太复杂的逻辑。
3.transfer()
如果转账失败,会自动revert
(回滚交易)。
send:
1.用法是接收方地址.send(发送ETH数额)
。
2.send()
的gas
限制是2300
,足够用于转账,但对方合约的fallback()
或receive()
函数不能实现太复杂的逻辑。
3.send()
如果转账失败,不会revert
。
4.send()
的返回值是bool
,代表着转账成功或失败,需要额外代码处理一下。
call:
1.用法是接收方地址.call{value: 发送ETH数额}("")
。
2.call()
没有gas
限制,可以支持对方合约fallback()
或receive()
函数实现复杂逻辑。
3.call()
如果转账失败,不会revert
。
4.call()
的返回值是(bool, bytes)
,其中bool
代表着转账成功或失败,需要额外代码处理一下。
调用已部署合约
在Solidity
中,一个合约可以调用另一个合约的函数,这在构建复杂的DApps时非常有用。
1.传入合约地址
2.传入合约变量
3.创建合约变量
4.调用合约并发送ETH
call:
call
是address
类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, bytes memory)
,分别对应call
是否成功以及目标函数的返回值。
call
是Solidity
官方推荐的通过触发fallback
或receive
函数发送ETH
的方法。
delegatecall:
delegatecall
与call
类似,是Solidity
中地址类型的低级成员函数。
一个投资者(用户A
)把他的资产(B
合约的状态变量
)都交给一个风险投资代理(C
合约)来打理。执行的是风险投资代理的函数,但是改变的是资产的状态。
目前delegatecall
主要有两个应用场景:
1.代理合约(Proxy Contract
):将智能合约的存储合约和逻辑合约分开:代理合约(Proxy Contract
)存储所有相关的变量,并且保存逻辑合约的地址;所有函数存在逻辑合约(Logic Contract
)里,通过delegatecall
执行。当升级时,只需要将代理合约指向新的逻辑合约即可。
2.EIP-2535 Diamonds(钻石):钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石是具有多个实施合约的代理合约。
两个合约变量存储布局必须相同
B通过call来调用C的setVars()函数,将改变合约C里的状态变量
B通过delegatecall来调用C的setVars()函数,将改变合约B里的状态变量
在以太坊链上,用户(外部账户,EOA
)可以创建智能合约,智能合约同样也可以创建新的智能合约。
有两种方法可以在合约中创建新合约,create
和create2
create
的用法很简单,就是new
一个合约,并传入新合约构造函数所需的参数
CREATE2
操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址
CREATE2
的用法和之前讲的CREATE
类似,同样是new
一个合约,并传入新合约构造函数所需的参数,只不过要多传一个salt
参数
计算时,需要将参数和initcode一起进行打包
create2的实际应用场景
1.交易所为新用户预留创建钱包合约地址。
2.由 CREATE2
驱动的 factory
合约,在Uniswap V2
中交易对的创建是在 Factory
中调用CREATE2
完成。这样做的好处是: 它可以得到一个确定的pair
地址, 使得 Router
中就可以通过 (tokenA, tokenB)
计算出pair
地址, 不再需要执行一次 Factory.getPair(tokenA, tokenB)
的跨合约调用。
selfdestruct
selfdestruct
命令可以用来删除智能合约,并将该合约剩余ETH
转到指定地址。selfdestruct
是为了应对合约出错的极端情况而设计的
1.已经部署的合约无法被SELFDESTRUCT
了。
2.如果要使用原先的SELFDESTRUCT
功能,必须在同一笔交易中创建并SELFDESTRUCT
。
注意
1.对外提供合约销毁接口时,最好设置为只有合约所有者可以调用,可以使用函数修饰符onlyOwner
进行函数声明。
2.当合约中有selfdestruct
功能时常常会带来安全问题和信任问题,合约中的selfdestruct功能会为攻击者打开攻击向量(例如使用selfdestruct
向一个合约频繁转入token进行攻击,这将大大节省了GAS的费用,虽然很少人这么做),此外,此功能还会降低用户对合约的信心。
ABI是与以太坊智能合约交互的标准。数据基于他们的类型编码;并且由于编码后不包含类型信息,解码时需要注明它们的类型.
Solidity
中,ABI编码
有4个函数:abi.encode
, abi.encodePacked
, abi.encodeWithSignature
, abi.encodeWithSelector
。而ABI解码
有1个函数:abi.decode
,用于解码abi.encode
的数据。
abi.encode
将给定参数利用ABI规则编码。ABI
被设计出来跟智能合约交互,他将每个参数填充为32字节的数据,并拼接在一起。如果要和合约交互,要用的就是abi.encode
abi.encodePacked
将给定参数根据其所需最低空间编码。它类似 abi.encode
,但是会把其中填充的很多0
省略。比如,只用1字节来编码uint8
类型。当想省空间,并且不与合约交互的时候,可以使用abi.encodePacked
abi.encodeWithSignature
与abi.encode
功能类似,只不过第一个参数为函数签名
。当调用其他合约的时候可以使用。
abi.encodeWithSignature
与abi.encodeWithSignature
功能类似,只不过第一个参数为函数选择器
,为函数签名
Keccak哈希的前4个字节。
abi.decode
abi.decode
用于解码abi.encode
生成的二进制编码,将它还原成原本的参数。
Keccak256
函数是Solidity
中最常用的哈希函数
函数选择器
当我们调用智能合约时,本质上是向目标合约发送了一段calldata
,发送的calldata
中前4个字节是selector
(函数选择器)。
calldata
就是告诉智能合约,我要调用哪个函数,以及参数是什么。
method id
定义为函数签名
的Keccak
哈希后的前4个字节,当selector
与method id
相匹配时,即表示调用该函数。在同一个智能合约中,不同的函数有不同的函数签名,因此我们可以通过函数签名来确定要调用哪个函数。
映射类型参数通常有:contract
、enum
、struct
等。在计算method id
时,需要将该类型转化成为ABI
类型。0
try-catch
在Solidity
中,try-catch
只能被用于external
函数或创建合约时constructor
(被视为external
函数)的调用。
ERC20
ERC20
是以太坊上的代币标准,它实现了代币转账的基本逻辑:
账户余额(balanceOf())
转账(transfer())
授权转账(transferFrom())
授权(approve())
代币总供给(totalSupply())
授权转账额度(allowance())
代币信息(可选):名称(name()),代号(symbol()),小数位数(decimals())
IERC20
IERC20
是ERC20
代币标准的接口合约,规定了ERC20
代币需要实现的函数和事件。 之所以需要定义接口,是因为有了规范后,就存在所有的ERC20
代币都通用的函数名称,输入参数,输出参数。 在接口函数中,只需要定义函数名称,输入参数,输出参数,并不关心函数内部如何实现。
代币水龙头
代币水龙头就是让用户免费领代币的网站/应用。
空投 Airdrop
空投是币圈中一种营销策略,项目方将代币免费发放给特定用户群体。为了拿到空投资格,用户通常需要完成一些简单的任务,如测试产品、分享新闻、介绍朋 友等。项目方通过空投可以获得种子用户,而用户可以获得一笔财富,两全其美。
因为每次接收空投的用户很多,项目方不可能一笔一笔的转账。利用智能合约批量发放ERC20
代币,可以显著提高空投效率
ERC165
智能合约可以声明它支持的接口,供其他合约检查
IERC721
IERC721
是ERC721
标准的接口合约,规定了ERC721
要实现的基本函数。它利用tokenId
来表示特定的非同质化代币,授权或转账都要明确tokenId
荷兰拍卖
荷兰拍卖(Dutch Auction
)是一种特殊的拍卖形式。 亦称“减价拍卖”,它是指拍卖标的的竞价由高到低依次递减直到第一个竞买人应价(达到或超过底价)时击槌成交的一种拍卖。
Merkle Tree
Merkle Tree
,也叫默克尔树或哈希树,是区块链的底层加密技术,被比特币和以太坊区块链广泛采用。Merkle Tree
是一种自下而上构建的加密树,每个叶子是对应数据的哈希,而每个非叶子为它的2
个子节点的哈希。
Merkle Tree
允许对大型数据结构的内容进行有效和安全的验证(Merkle Proof
)。对于有N
个叶子结点的Merkle Tree
,在已知root
根值的情况下,验证某个数据是否有效(属于Merkle Tree
叶子结点)只需要ceil(log₂N)
个数据(也叫proof
),非常高效。如果数据有误,或者给的proof
错误,则无法还原出root
根植。
数字签名
以太坊使用的数字签名算法叫双椭圆曲线数字签名算法(ECDSA
),基于双椭圆曲线“私钥-公钥”对的数字签名算法
1.身份认证:证明签名方是私钥的持有人。
2.不可否认:发送方不能否认发送过这个消息。
3.完整性:通过验证针对传输消息生成的数字签名,可以验证消息是否在传输过程中被篡改。
随机数
使用链上随机数高效,但是不安全。
而链下随机数生成依赖于第三方提供的预言机服务,比较安全,但是没那么简单经济。
ERC1155
ERC1155
允许一个合约包含多个同质化和非同质化代币。ERC1155
在GameFi应用最多,Decentraland、Sandbox等知名链游都使用它。
WETH
WETH
(Wrapped ETH)是ETH
的带包装版本。我们常见的WETH
,WBTC
,WBNB
,都是带包装的原生代币。在2015年,ERC20标准出现,该代币标准旨在为以太坊上的代币制定一套标准化的规则,从而简化了新代币的发布,并使区块链上的所有代币相互可比。不幸的是,以太币本身并不符合ERC20
标准。WETH
的开发是为了提高区块链之间的互操作性 ,并使ETH
可用于去中心化应用程序(dApps)。它就像是给原生代币穿了一件智能合约做的衣服:穿上衣服的时候,就变成了WETH
,符合ERC20
同质化代币标准,可以跨链,可以用于dApp
;脱下衣服,它可1:1兑换ETH
。
分账合约具有以下几个特点:
1.在创建合约时定好分账受益人payees
和每人的份额shares
。
2.份额可以是相等,也可以是其他任意比例。
3.在该合约收到的所有ETH
中,每个受益人将能够提取与其分配的份额成比例的金额。
4.分账合约遵循Pull Payment
模式,付款不会自动转入账户,而是保存在此合约中。受益人通过调用release()
函数触发实际转账。
线性释放
线性释放指的是代币在归属期内匀速释放。
代币锁
代币锁(Token Locker)是一种简单的时间锁合约,它可以把合约中的代币锁仓一段时间,受益人在锁仓期满后可以取走代币。代币锁一般是用来锁仓流动性提供者LP
代币的。
时间锁
在区块链,时间锁被DeFi
和DAO
大量采用。它是一段代码,他可以将智能合约的某些功能锁定一段时间。它可以大大改善智能合约的安全性
代理合约
代理模式将合约数据和逻辑分开,分别保存在不同合约中。我们拿上图中简单的代理合约为例,数据(状态变量)存储在代理合约中,而逻辑(函数)保存在另一个逻辑合约中。代理合约(Proxy)通过delegatecall
,将函数调用全权委托给逻辑合约(Implementation)执行,再把最终的结果返回给调用者(Caller)。
代理模式主要有两个好处:
1.可升级:当我们需要升级合约的逻辑时,只需要将代理合约指向新的逻辑合约。
2.省gas:如果多个合约复用一套逻辑,我们只需部署一个逻辑合约,然后再部署多个只保存数据的代理合约,指向逻辑合约。
可升级合约
是一个可以更改逻辑合约的代理合约
选择器冲突
函数选择器(selector)是函数签名的哈希的前4个字节相同
透明代理
管理员可能会因为“函数选择器冲突”,在调用逻辑合约的函数时,误调用代理合约的可升级函数。那么限制管理员的权限,不让他调用任何逻辑合约的函数,就能解决冲突:
管理员变为工具人,仅能调用代理合约的可升级函数对合约升级,不能通过回调函数调用逻辑合约。
其它用户不能调用可升级函数,但是可以调用逻辑合约的函数。
UUPS(universal upgradeable proxy standard,通用可升级代理)将升级函数放在逻辑合约中。这样一来,如果有其它函数与升级函数存在“选择器冲突”,编译时就会报错。
多签钱包
多签钱包是一种电子钱包,特点是交易被多个私钥持有者(多签人)授权后才能执行:例如钱包由3个多签人管理,每笔交易需要至少2人签名授权。多签钱包可以防止单点故障(私钥丢失,单人作恶),更加去中心化,更加安全,被很多DAO采用。