top of page
hero.png

免费 Solidity 教程

这是给谁的?

无需电子邮件!这是为了经验丰富的程序员 谁想要快点进入正题立即练习 他们刚刚获得的信息高度优化的主题顺序。我们强调意想不到的和不寻常的 Solidity 语言的各个方面,同时掩盖了我们可以合理地认为对有能力的开发人员来说显而易见的事情。除了偶尔的幽默评论之外,我们使教程尽可能简短(但不更短)。虽然这是一个 Solidity 初学者教程,但它是针对经验丰富的编码人员的。

准备好体验RareSkills的学习效率吧!

为什么本教程更优秀(除了免费之外)

很久以前创建的教程使用旧版本的 Solidity。

从 Solidity 0.8.19(2023 年 2 月 22 日发布)开始,我们的教程是最新的。

另一个显着的区别是我们从一开始就教授代工开发环境。此前,Truffle 和 Hardhat 是行业标准。截至 2023 年,Foundry 是占主导地位的开发框架。

Solidity 语言是不难学。 它看起来很像 javascript(或者 Dart,对于那些了解该语言的人来说)。本教程假设您已经知道如何使用流行语言进行编码,并利用这些知识快速学习 Solidity。 您应该已经知道什么是函数、整数、字符串、数组等。如果您是第一次学习编码,本教程不适合您。它适合经验丰富的编码人员快速掌握 Solidity。

本教程的一个重要部分是你会花更多的时间编码而不是阅读材料或观看视频。 我们将为您提供足够的信息来帮助您入门,然后提供一些旨在强化您刚刚学到的知识的练习题。练习题是学习语言的一个非常重要的部分。如果您只是好奇,可以直接阅读内容。但如果你想真正学习Solidity,你需要做练习题。完成后,查看或建议初学者 Solidity 项目 强化所学知识。 

不要被这个课程是免费的事实所迷惑。这不是轻松的内容。本教程由 Jeffrey Scholz 设计,他因 Udemy 上唯一的专家级 Solidity 课程而两次成为畅销书。练习问题是由另外 3 名经验丰富的 Solidity 工程师组成的团队创建的。我们强烈建议您将我们的教学大纲与其他付费课程进行比较,以便亲自了解。

我们并不羞于我们的工作质量。

RareSkills 的免费内容优于其他地方的付费内容。

当其他人对这样的内容收取数百美元的费用时,为什么我们要免费提供呢?

因为我们希望稍后,至少你们中的一些人会参加我们的高级课程Solidity 训练营,这是付费的。

了解一门语言和了解一个领域之间是有区别的。了解 Python 并不能让你成为一名数据科学家,了解 Javascript 并不能让你成为一名前端开发人员,了解 Kotlin 并不能让你成为一名 Android 开发人员。同样,了解 Solidity 并不能让你成为以太坊智能合约开发人员。

然而,Solidity 是开发智能合约的先决条件。我们在高级 Solidity 训练营中教授智能合约开发。显然,该课程假设您了解 Solidity。

本教程的材料和练习题都是 100% 免费,不需要登录、信用卡或您提供的任何信息。我们希望您能从这一宝贵资源中受益,或许还可以参加我们的培训计划并加入我们令人惊叹的社区!

执照

我们免费提供信息,而其他人却要收费,有时要收费数百美元。为了防止滥用,请了解我们的版权条款。

尽管此信息是免费的,但重新分发、修改或复制并非免费。未经授权进行任何形式的重新分发。项目中的源代码已获得许可商业来源许可证。严禁重新分发、复制或创建衍生作品。

如果您想与其他人分享此内容,请仅提供此页面的超链接。

开始吧

开发环境

 

首先,我们将学习 Solidity 作为一门语言。我们不会从在区块链上部署合约开始,这只会使事情变得更加复杂。

前往remix.ethereum.org

强烈建议您使用 Remix 来完成本课程中的示例。

让我们创建一个你好世界。

访问 remix.ethereum.org 后,右键单击“合约”,然后左键单击“新建文件”

Solidity Remix IDE 创建新的 Solidity 文件

 

这是一个 Solidity 文件,因此请为该文件提供 .sol 扩展名。名字并不重要

Solidity Remix IDE .sol 文件扩展名

 

复制上面的代码,或者更好的是,您自己输入它。

将 Solidity 代码粘贴到 Remix IDE 中

 

要编译代码,请在 mac 上按 Command S(在 Windows 上按 ctrl S)。如果您在实体符号上方看到红色气泡,则表示存在语法错误。如果您看到橙色,则表示您只有暂时可以忽略的警告。

现在部署功能。单击左侧的以太坊符号,然后单击部署。

如何在 remix ide 中部署智能合约

 

要测试这些功能,请向下滚动左侧菜单,然后单击它们。他们将返回您期望的值。

在 remix ide 中测试 Solidity 智能合约

 

如果我们想做出改变怎么办?单击垃圾桶图标即可删除合同。

使用垃圾桶图标删除 Remix IDE 中的智能合约

 

现在更改代码,使用命令 S 重新编译,然后单击部署。再次测试功能。

在 remix ide 中重新编译 Solidity 智能合约

 

如果函数需要参数,它将提供在按钮旁边。

测试在 remix ide 中接受参数的 Solidity 函数

 

您现在已经准备好尝试 Solidity 智能合约了!

固定大小数据类型:Solidity 是一种类型语言。

 

Solidity 是一种类型化语言。

与 javascript 或 python 不同,您可以将 bool 或字符串或数字分配给变量,每个变量只能有一种类型,并且必须明确声明。

这也适用于函数。您必须显式指定参数类型和返回类型。

现在让我们讨论最常用的类型:

  • 无符号整数,或uint256

  • 布尔变量或布尔值

  • 地址 type,存储以太坊钱包地址或智能合约地址

 

Solidity 有数组、字符串、结构体和其他类型,但它们需要稍微不同的处理,所以我们稍后再讨论它们。

让我们看一下返回每种类型的三个不同函数。

 

在这些示例中,我们将值分配给变量然后返回它。我们当然可以像这样直接返回值。

 

函数签名与返回类型匹配非常重要。下面的代码会产生错误

地址

地址表示为包含 40 个字符的十六进制字符串,并且始终以 0x 开头。有效的十六进制字符串包含字符 [0-9] 或 [a-f](含)。

警告:手动输入地址时要小心。 Solidity 会将 0x1 转换为值为 0x0000000000000000000000000000000000000001 的地址。如果您的地址少于 40 个十六进制字符,它将用前导零填充。

 

如果您创建的地址超过 40 个字符,它将无法编译。

请注意,40 个字符不包括前导 0x。

uint256

让我们回顾一下uint256这究竟意味着什么?

u 表示无符号。它不能表示负数。 256 意味着它最多可以存储数字256位大,即 2^256-1。

让我们将其插入 python 中看看这个数字有多大。

 

这是一个非常大的数字,足以满足您在区块链上执行的几乎所有操作。

这将在 Solidity 中编译

但如果数字更大,代码将无法编译。

正如你可以想象的那样,一个uint128存储大小最大为 2^128 - 1 的无符号数字。

大多数时候,您应该只使用 uint256。使用 uint64 或 uint128 等较小类型的时间是一个更高级的主题。只要坚持 uin现在是t256。

布尔类型

这个非常明显,就像其他语言一样。布尔变量保存 true 或 false。就是这样。

练习题

如果您尚未在系统上设置 cURL,可以访问以下链接进行设置:

卷曲设置

您应该看到如下所示的输出

这里的“锻造”和“铸造”到底是什么?

您可以将其视为 javascript 的 gulp 或 webpack、java 的 maven 或 python 的 tox。 Foundry 是一个让测试、开发和部署变得更容易的开发框架。毫无疑问,它是 2023 年最流行的框架,绝对值得作为 Solidity 开发人员了解。

它真正酷的一件事是您可以在 Solidity 中编写单元测试,从而使测试变得更容易。以前的工具使用 JavaScript,这会强制在语言之间进行上下文切换,并使类型转换变得有点棘手。

VS 代码扩展

如果您还没有下载以下扩展程序,那么您应该下载!

Solidity vscode 扩展的屏幕截图

算术

 

Solidity 中的算术与其他语言中的算术行为完全相同,因此我们不会在这里详细说明这一点。

您可以通过这种方式添加数字

指数与其他类似 c 语言中的指数相同。

模数也是如此

减法、乘法和除法是显而易见的,所以我不会教你如何做这些来侮辱你的智力。

Solidity 没有浮动

如果你尝试用 5 除以 2,你不会得到 2.5。你会得到 2。记住,unit256 是一个无符号整数。所以你所做的任何除法都是整数除法。

但如果您确实想知道 200 的 10% 是多少呢?对于计算利息来说,这似乎非常合理。

 

解决方案是将 x * 0.1 转换为 x * 1 / 10。这是有效的,并且会产生正确的答案。

 

如果您的利息是 7.5% 之类的金额,那么您需要执行以下操作

 

如果您想知道一个城市相对于一个国家的人口百分比,您不能执行以下操作。

这需要我们稍后描述的更高级的解决方案。

笔记: 为什么 Solidity 不支持浮动?浮动并不总是确定性的,区块链必须是确定性的,否则节点将不会就交易的结果达成一致。例如,如果除以 2/3,某些计算机将返回 0.6666,而其他计算机将返回 0.66667。这种分歧可能会导致区块链网络分裂!因此,solidity 不允许浮动。

坚固性不t 下溢或上溢,停止执行

如果你尝试会发生什么 执行以下操作?

如果 x 是 2 并且 y 是 5 会发生什么?你不会得到负数 3。实际上,执行会因以下原因而停止:恢复

Solidity 不会抛出异常,但你可以想到一个恢复相当于其他语言中的未捕获异常或恐慌。

过去,可靠性会允许溢出和下溢,但这会导致足够多的智能合约被破坏或被黑客攻击,以至于该语言在语言中内置了溢出和下溢保护。该功能是在 Solidity 0.8.0 版本之后添加的。

您现在可能已经注意到很多 Solidity 文件都有一行

这意味着源代码是使用0.8.0或更高版本编译的。如果您看到早于该版本的版本,则不能假设代码中内置了溢出保护。

如果要允许下溢和溢出,则需要使用未经检查的块

您可以使用未经检查的块来允许下溢和溢出。除非您有充分的理由这样做,否则不建议这样做。未检查的块可以像这样使用:

请注意,未经检查的块内的任何内容即使溢出或下溢也不会恢复。这是一项非常高级的功能,除非您知道自己在做什么,否则不应使用。

如果语句

 

If 语句的行为与其他语言完全相同

 

if 语句中的参数必须是布尔值。请注意,上面的代码与以下代码等效。

 

与 Python 或 javascript 等动态语言不同,您不能执行以下操作

 

Solidity 还支持“else if”结构,但我们假设您已经熟悉它的样子。

 

Solidity 没有转变像Java和C那样的语句。

练习题

If语句

For 循环

 

就像 if 语句一样,for 循环也没什么奇怪的。这是将 1 到 99 之间的所有数字相加的代码

 

如果您愿意的话,Solidity 还支持 += 运算符。

 

坚固性也有尽管循环和执行 while 循环 但这些很少被使用,所以此时不值得一提。

 

for 循环的一个非常自然的用例是迭代数组。但我们还没有介绍数组,所以我们会在那时解释它。

 

与其他语言一样,您可以从 for 循环内的函数提前返回。这段代码将从 2 循环到数字,直到找到质因数。

练习题

是素数

斐波那契

数组和字符串简介

 

在本节中我们将介绍大批数据结构和细绳 数据结构。它们的行为与我们之前讨论的 Solidity 数据类型不同,因此我们将在这里讨论它们。

声明数组的语法

让我们看一个函数,它需要一个 数组并返回一个数组。这里有很多东西需要解压!

首先,应该清楚声明数字数组的语法是uint256[]。稍后我们将讨论“calldata”和“内存”。

 

如果您想要一个地址或布尔值数组,则如下所示:

 

那么这个calldata和内存位是什么?首先,如果不包含它们,代码将无法编译。以下是两个无法编译的代码示例。

 

那么calldata和内存是什么?

如果您熟悉 C 或 C++,这个概念就会很直观。 Solidity 中的内存就像 C、C++ 或 Rust 中的堆。数组的大小可以不受限制,因此将它们存储在执行堆栈上(如果您不知道那是什么,请不要担心),可能会导致堆栈溢出错误(不要与著名的论坛混淆!)。

Calldata 是 Solidity 特有的东西。它是当某人将交易传输到区块链时发送的实际“交易数据”。

Calldata 的意思是“指以太坊交易本身的数据”。这是一个相当高级的概念,所以如果您现在不完全理解它,请不要担心。

如有疑问:数组和字符串的函数参数应为 calldata,返回类型的函数参数应为内存。

在函数参数中使用“calldata”有一些例外,但数组的返回类型应该总是是内存,切勿调用data,否则代码将无法编译。为了避免信息轰炸,我们稍后会讨论 calldata 的异常。

以下是如何在 Remix 中使用数字数组。

如何在 remix ide 中输入数字数组

与其他语言一样,数组的索引为零

这里没有什么惊喜。

 

请注意,返回类型是 uint256,因为我们返回的是数字,而不是数组。

请注意,如果数组为空,事务将恢复。

要获取数组的长度,请使用 .length

这与 JavaScript 相同。

 

这也是循环数组的方法。

数组可以声明为具有固定长度

在前面的示例中,声明期间方括号内没有任何内容。如果要强制数组具有固定大小,可以将大小放在方括号内。

 

如果该函数传递除 5 之外的任何大小的数组,它将恢复。

弦乐

字符串的行为与数组非常相似。事实上,它们是底层的数组(但有一些区别)。这是一个返回您传递给它的字符串的函数。

 

最后是你好世界。

 

连接字符串

有趣的是,直到 2022 年 2 月 Solidity 0.8.12 发布时,Solidity 才支持字符串连接。如果你想在 Solidity 中进行字符串连接,请确保杂注文件顶部至少为 0.8.12

 

这么晚才添加对串联的支持是有原因的,智能合约通常处理数字,而不是字符串。

字符串无法建立索引

在 javascript 或 python 等语言中,您可以像索引数组一样索引字符串并获取字符。 Solidity 无法做到这一点。以下代码无法编译

字符串不支持长度

Solidity不支持获取stri的长度ng。这是b因为 unicode 字符会使长度不明确,而 Solidity 将字符串表示为字节数组,而不是字符序列。

我们遗漏了什么

  • Solidity 中的数组支持如下操作流行音乐(),但是这有更高级的副作用,所以我们稍后会教这个。

  • 在函数内部声明数组和字符串(与在参数或返回值中声明数组和字符串不同)具有不同的语法

 

练习题

菲兹巴兹

求和数组

过滤奇数

已排序

意思是

嵌套数组

 

嵌套数组在实践中很少使用,但为了完整起见,我们将它们包含在此处。

嵌套数组,顾名思义,是指包含在另一个数组中的数组。

 

在此示例中,该函数接收矩形网格。

 

这里它正在混音中运行。

在 remix ide 的 Solidity 函数中输入嵌套的数字数组

 

您还可以从 2D 数组获取 1D 数组

 

要声明固定大小的数组,请使用以下语法

 

可能令人困惑的是,当您访问数组中的特定项目时,顺序可能感觉与其他语言相反,但如果您仔细考虑一下,这是有道理的。

 

就像一维数组一样,如果访问越界区域,事务将会恢复。

请注意,嵌套数组在实践中极为罕见。如果您想跳过本节,请随意。

问题

嵌套数组

井字游戏

存储变量

 

到目前为止,我们所有的函数都只是返回完全依赖于函数参数的值。除了立即输入之外,它们不依赖于任何其他东西。这就是为什么他们被称为纯的功能。他们不知道区块链状态或过去发生的任何事情。

如果我们要跟踪某些事情,比如欠某个人多少钱,或者他们在游戏中得到多少分,这将是一个很大的问题。

现在我们介绍的是存储变量。

 

这些看起来像其他语言中的“类变量”,但实际上的行为并不像它们。你可以将它们视为变量行为就像一个微型数据库。

让我们看一个例子

 

我们这里有很多东西要打开!

在函数外部声明的变量是存储变量。他们在交易结束后保留其价值。

注意获取X() 有修饰符看法 代替纯的。那是因为它查看区块链状态,即变量 x 中存储了什么。如果在此示例中将视图更改为纯视图,则代码将无法编译。你还可以想到看法 作为只读。另请注意,getX 的返回值与 x 的类型相同,均为 uint256。

其次,请注意设置X 没有视图或纯修饰符。那是因为它是一个状态改变函数。更改存储变量或对区块链进行其他持久更改的函数不能具有 view 或 pure 修饰符,这是因为它们不是只读 因此不能被标记为视图,当然也不能纯的

为了强制执行这一点,请注意以下代码无效

 

注意变量x本身有修饰符内部的。这意味着其他智能合约无法看到该价值。

仅仅因为变量是内部变量并不意味着它是隐藏的。它仍然存储在区块链上,任何人都可以解析区块链以获取价值!

这就是事情变得混乱的地方。

下面的代码也是有效的,但它被认为是不好的做法。

 

在本例中,我们删除了 x 的内部修饰符,它仍然可以编译。这被认为是不好的做法,因为您没有明确表达 X 可见性的意图。

下面的代码也是有效的

 

当变量被声明为公共时,意味着其他智能合约可以读取该值但不能修改它。

这很令人困惑,因为公共函数可以修改变量,但公共变量不能被修改,除非有一个函数可以改变它们的值。

概括

  • 存储变量在函数外部声明

  • 没有视图或 pure 修饰符的公共函数可以更改存储变量

  • 纯函数无法访问存储变量

存储中的数组

 

您可能已经注意到,在我们关于数组的部分中,我们好奇地省略了

  • 写入数组中的索引

  • 附加到数组

  • 从数组中弹出

 

这是因为您很少对作为函数参数提供的数组执行此操作。

然而,当数组在存储中时,这种操作更为常见。

这是一些示例代码

 

我建议您将此代码复制并粘贴到混音 这样你就可以对正在发生的事情有一个直觉。

调用 setArray 方法[1,2,3,4,5,6]

现在打电话获取长度() 它返回 6,这是数组的长度。

现在打电话添加到数组 带参数 10. 调用获取长度() 再次。现在它返回 7。

称呼 从数组中删除() 其次是获取长度()。现在如您所料返回 6。

由于 myArray 是公共的,因此 Remix 将其显示为可见的函数。但它不会返回整个数组。它将请求一个索引并返回该索引处的值。 myArray 函数的行为如下

值得注意的是,由于 myArray 是公共的,Remix 将其显示为与函数一样可见。这意味着编译器将自动生成一个名为 myArray() 的函数,可以调用该函数来读取 myArray 中存储的值。

Solidity 不返回数组,它只能返回您要求的索引。 Remix IDE 的屏幕截图。

 

但它不会返回整个数组。它将请求一个索引并返回该索引处的值。 myArray 函数的行为如下

 

然而,该函数获取整个数组() 返回整个数组。

注意流行音乐() 不返回值。

删除项目

Solidity 没有办法删除itemm 放在列表中间并将长度减一。以下代码有效,但不会更改列表的长度。

 

如果您想删除一个项目并减少长度,则必须执行“弹出并交换”。

它删除索引参数处的元素并将其与数组中的最后一个元素交换

 

Solidity 无法从列表中间删除并保留数组的原始顺序。

弦乐

字符串的行为与数组类似,但当它们是公共的时,它们返回整个字符串,因为字符串无法索引(令人困惑,不是吗?)。字符串没有弹出或长度操作。

练习题

数字列表

插入数组

映射

 

映射、哈希图、关联数组、映射,无论你想怎么称呼它,solidity 都有。

我们将其称为“映射”,因为这是 Solidity 使用的关键字。让我们看一个例子。

 

这正如你所想的那样。因为 myMapping 是公共的,所以 Solidity 用 getter 函数包装它,您可以直接访问这些值。但是,如果您想通过函数访问地图,您可以遵循以下模式获取值

这是第一件令人惊讶的事情

如果您使用尚未设置的密钥访问映射,您将不会得到恢复。映射将仅返回该值的数据类型的“零值”

在以下示例中,如果您提供尚未设置的数字,则映射将返回 false。

 

我鼓励您将此代码粘贴到 remix 中,然后插入密钥的数字以查看返回的零值。

映射.png

 

顺便说一句,ERC20 代币使用映射来存储某人拥有多少代币!他们将地址映射到某人拥有多少代币。

 

这个实现有一个缺陷,任何人都可以调用公共函数并在地址之间随意发送令牌,但我们稍后会修复这个问题。

违反直觉的是,ERC20 代币并不存储在加密货币钱包中,它们只是一个uint256 与您在智能合约中的地址相关联。我们认为“ERC20 代币”只是一个智能合约。

这是 ERC20 代币 USDC 的智能合约:https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48

这是 ApeCoin 的代币,Bored Ape Yacht Club 生态系统的货币

https://etherscan.io/token/0x4d224452801aced8b2f0aebe155379bb5d594381

惊喜 1:映射只能声明为存储,不能在函数内声明它们

这似乎是一个非常奇怪的限制,但这与以太坊虚拟机的工作方式有关。区块链一般不喜欢哈希图,因为它们的运行时间不可预测。以下代码无效。

惊喜 2:映射无法迭代

无法迭代映射的键。每个密钥在技术上都是有效的,只是默认为零。

惊喜3:映射无法返回

以下代码无效。映射不是 Solidity 函数的有效返回类型。

 

问题

特殊号码

嵌套映射

 

在大多数语言中,一个哈希图可以包含另一个哈希图,solidity 也这样做。但是,由于映射不是有效的返回类型,因此您必须提供映射所需的所有键。

让我们看一个例子

 

与嵌套数组不同,嵌套映射在智能合约中非常常见。例如,您可以这样进行簿记

 

请注意,此处的顺序很重要。在这种结构中,一个贷款人可以有多个借款人。如果我们将借款人设置为第一个键,则意味着借款人可能有多个债务人。

适用于常规映射的相同限制也适用于嵌套映射。您不能迭代键、在函数内声明它们或从函数返回它们。

公共嵌套映射不起作用

这是坚固性的另一个奇怪的怪癖。当您将变量声明为公共时,Solidity 会自动为变量创建获取函数。但是,公共 getter 函数允许您提供必要的参数。

是的。你没看错。

解决方案是将嵌套映射设为私有并将它们包装在获取其值的公共函数中。是时候练习一下了!

msg.sender 和地址(this)

还记得我们之前的错误 ERC20 代币示例吗?

又来了

 

问题是我们不知道谁在调用该函数。

幸运的是,solidity 有一种机制可以识别谁在调用智能合约:消息发送者。 Msg.sender 返回调用智能合约函数的地址。

在 remix 中尝试以下代码

 

它将返回您在混音中使用的测试地址。

现在,通过点击“帐户”下拉列表来更改测试地址。然后再次尝试该功能。返回的地址会有所不同。

消息发送器.png

 

通过将 msg.sender 与 if 语句结合起来,您可以赋予某些地址特殊权限。

假设我们希望 remix 中的默认地址是特殊地址。

 

上面的代码允许人们查看他们的余额(因为余额是一个公共变量),但只有银行家可以更改它。

通过聪明一点,我们实际上可以允许人们将余额转移给其他人,而无需银行家为他们做这件事。考虑以下示例。

 

功能转移任何人都可以调用。但是,它只能借记(扣除)余额消息发送者。作为读者的练习,我鼓励您思考为什么不可能使用转移

一个自然的问题是,如果有人尝试发送的金额超出了他们的余额,会发生什么?如果您使用 Solidity 0.8.0 或更高版本,则不会发生任何情况。交易会恢复,因为您无法减去无符号数使其变为负数。

tx.orig

还有另一种机制可以获取发送者,tx.origin。尽管它的行为与 msg.sender 类似,但您不应该使用它。为了避免现在用太多信息轰炸您,我们不会解释周围的安全问题tx.origin 然而。但重要的一点是,除非在非常特殊的情况下,否则不要使用 tx.origin。

地址(这个)

智能合约可以通过以下代码知道自己的地址


在 Remix 中尝试一下并查看地址匹配
 

a screenshot in remix of a smart contract returning its own address

 

问题

谁叫我

构造函数

 

回到我们的滚动 ERC20 示例,我们做了一些有点奇怪的事情,我们直接在合约中设置银行家变量。

 

没关系,但是如果有人想部署合约并将自己设置为银行家怎么办?

智能合约有一个特殊的函数,在部署时调用,称为构造函数。这与其他面向对象的编程语言非常相似。这是它的样子

 

请注意,它是“constructor()”而不是“function constructor()”,并且我们没有指定民众因为构造函数不能用 pure、view、public 等来修改。

如果您希望银行家由部署合约的人员配置,那么您可以将其用作函数参数。

 

顺便说一下,你会在构造函数中经常看到这种模式:variable = _variable。 Solidity 并不要求您这样做,但它被认为是传统的。

在 Remix 上部署具有构造函数参数的合约时,您必须将参数放入“部署”旁边的框中。

remix 中的 Solidity 构造函数参数

与其他函数不同,calldata不能用于数组和字符串,必须使用内存

同样,由于我们现在无法了解的原因,calldata 不能在构造函数参数中使用。我知道,这似乎是一个非常奇怪和随机的限制,但当你了解以太坊的底层工作原理之后,它就会变得有意义。

这是在构造期间设置字符串的方法

 

您可能会想只在任何地方使用内存而不费心使用 calldata。但现在值得记住这一点,因为 calldata 会带来更便宜的交易(即用户的汽油费更低)。

另外,如果你想知道,构造函数不能返回值。

问题

部署者

要求

 

只要再多一个基本的 Solidity 关键字,我们就可以创建自己的 ERC20 代币了。

虽然我们可以使用 if 语句来检查函数的输入是否有效,或者正确的 msg.sender 正在调用该函数,但优雅的方法是使用 require 语句。如果不满足某些条件,require 语句会强制事务恢复。

 

在 remix 中尝试上面的代码。

请注意,忽略错误消息是有效的,但被认为是不好的做法,因为它使理解失败变得更加困难。

 

您可以使用此结构来确保 msg.sender 是他们应该的身份。但你可以在以下问题中练习这一点。

练习题

不够

所有者

ERC20代币

 

我们现在准备制作 ERC20 代币!

ERC20 代币通常具有姓名 和一个象征。例如,ApeCoin 的名称为“ApeCoin”,但符号为“APE”。令牌的名称通常不会更改,因此我们将在构造函数中设置它,并且不提供任何稍后更改它的函数。我们将公开这些变量,以便任何人都可以检查合约的名称和符号。

 

接下来,我们需要存储每个人的余额。

 

我们说“balanceOf”是因为它是 ERC20 的一部分规格。 ERC20 作为规范意味着人们可以在合约上调用函数“balanceOf”,提供地址,并获取该地址拥有多少代币。

现在每个人的余额都是零,所以我们需要一种方法来使代币存在。我们将允许一个特殊的地址,即部署合约的人,随意创建代币。

 

通常的做法是函数 mint() 将 to 和 amount 作为参数参数。它允许合约部署者向其他帐户铸造代币。为了简单起见,函数 mint() 只允许 Mint 代币的部署者进入他的账户。

为了跟踪现有代币的数量,ERC20 规范需要一个名为的公共函数或变量总供应量 这告诉我们已经创建了多少代币。

 

如果您在钱包中使用过 ERC20 代币,那么毫无疑问您见过自己拥有一小部分代币的情况。当无符号整数没有小数时,这是如何发生的?

uint256 可以表示的最大数字是

115792089237316195423570985008687907853269984665640564039457584007913129639935

让我们稍微减少一下数字以使其更清楚

10000000000000000000000000000000000000000000000000000000000000000000000000000

为了能够描述“小数”,我们说右边的 18 个零是硬币的小数部分。

10000000000000000000000000000000000000000000000000000000000.0000000000000000000

因此,如果我们的 ERC20 有 18 位小数,我们最多可以有

10000000000000000000000000000000000000000000000000000000000

完整的硬币,右边的零是小数。那是 10 个十进制硬币,或者对于那些不熟悉如此无用的大数字的人来说,那是 1 万亿 x 1 万亿 x 1 万亿 x 1 万亿。

这对于大多数应用来说应该足够了,甚至是陷入恶性通货膨胀的国家。

货币的“单位”仍然是整数,但单位现在是非常小的值。

18 位小数是相当标准的,但有些硬币使用 6 位小数。

硬币的小数点不应该改变,它只是一个返回硬币有多少位小数的函数。

 

如果你注意的话,我确实在这里向你扔了一个曲线球。数字类型是 uint8,而不是 uint256。 uint8 只能表示 255 以内的数字。但是,uint256 有 77 个零(如果您想数一下上面数字的零,您可以验证这一点)。因此,如果你想拥有一枚完整的硬币,小数点后的位数不可能超过 77 位。因此标准规定我们使用 uint8,因为小数位数永远不会很大。

转移

现在让我们添加我们的t回传功能。

 

啊哈,我们在那里添加了一行额外的代码: require(to != 地址(0),“无法发送到地址(0))

为什么是这样?好吧,没有人“拥有”零地址,因此发送到那里的代币是不可花费的。按照惯例,将令牌发送到零地址应该会减少 总供应量 所以我们希望为此有一个单独的函数。

现在我们引入一个概念津贴

津贴

配额使地址能够花费其他人的代币,最多可达他们指定的限额。

为什么你会允许别人为你花费代币?这是一个很长的故事,但总而言之,想想你如何“知道”有人向你转移了 ERC20 代币。所发生的只是一个函数被执行并且映射改变了值。您没有“收到”代币,它们只是与您的地址相关联。

现在,作为区块链之外的实体,您可以检查它是否有让您变得更富有的事件。

然而,智能合约无法做到这一点。

智能合约作为转账接收者的既定模式是允许智能合约有一定的津贴,然后告诉智能合约从你的账户中提取余额。

当您想要将代币转移到智能合约时,典型的方法是首先批准智能合约从您的账户中提取一定数量的代币。然后,您指示智能合约从您的帐户中提取批准数量的代币。这是智能合约中使用的常见模式,用于将代币转移到合约。

让我们添加津贴跟踪器,以及向其他用户提供津贴的方法。

 

在 grant[msg.sender][spender] = amount; 行中,spender 指的是由 msg.sender 授予津贴的帐户地址。 msg.sender 允许花费者从其帐户中花费一定数量的代币。

因此,msg.sender 是代币的所有者,而支出者是经所有者批准代表其花费一定数量代币的人。

啊,但是我们没有办法实际使用所提供的津贴,它就放在那里!这就是transferFrom 的用途。

从转移

 

让我们解开我们刚刚在这里所做的事情。

首先,币的所有者可以调用transferFrom。在这种情况下,津贴毫无意义,因此我们不必费心检查津贴映射,并相应地更新余额。

否则,我们会检查消费者是否获得了足够的津贴,然后减去他们的支出金额。如果我们不减去他们的支出,我们将拥有无限的消费能力。

还有一项清理工作要做。如果我们阅读原始规范电子工业计划20 它说approve、transfer和transferFrom在成功后必须返回true。所以让我们添加一下。

 

冒着向您提供太多信息的风险,我们可以对此代码进行清理。注意从转移转移 其中有重复的代码。对此我们能做些什么呢?我们可以将余额更新代码分解为一个单独的函数,但我们需要确保该函数不公开,否则有人可以窃取硬币!

 

干净多了!

练习题

  • 修改上述代码,以便不允许超过 100 万个代币进入流通,即使所有者尝试铸造更多代币

元组

 

我们将在这里稍微切题地介绍元组数据类型,因为它是接下来的部分的先决条件。

如果您在 Python 或 Rust 等语言中使用过元组,那么这里并不奇怪。它是一个固定大小的数组,但其中的类型可以是混合的。

这是返回元组的函数的示例

 

请注意,元组是隐含的。关键字“tuple”从来不会出现在 Solidity 中。

元组也可以被“解包”以获取其中的变量,如下例所示。

 

与其他语言一样,元组的长度不需要为 2。它可以是 3、4 甚至更长。

练习题

图普莱多

应用程序二进制接口编码(abi 编码)

 

在我们介绍下一条信息之前,我们必须继续看似另一个随机切线的事情。

但我想让你明白以下内容是什么

abi.编码

abi解码

abi.encodeWithSignature

为了激励他们,让我们创建另一个智能合约,打开“调试”下拉菜单,并获取某条信息。

在 remix ide 中调试智能合约交易

 

当我们复制它时,我们得到

0x92d62db5

这到底是什么?这是函数签名 “meaningOfLifeAndAllExistence()”。稍后我们将了解这是如何得出的。

每当您“调用”智能合约时,您实际上是在发送附加了一些数据的以太坊交易,以便智能合约知道要执行哪个函数。

让我们从另一个角度来看看这些信息。

remix ide 显示了 Solidity 函数的返回值

 

我们将返回类型更改为“字节内存”(不用担心我们以前没有见过),并返回一个名为 msg.data 的变量(也不用担心我们以前没有见过) 。

需要注意的重要一点是我们得到了相同的字节序列!

那么到底发生了什么?

当您调用智能合约中的函数时,您实际上并不是在执行“函数调用”本身,而是向合约发送数据,其中包含有关应执行哪个函数的一些信息。

有道理吧?当您启动浏览器钱包并交易 ERC20 代币时,您无法远程“调用 ERC20 合约上的函数”。函数调用仅发生在同一执行上下文内。然而,将事务描述为函数是很方便的。但我们需要看看幕后到底发生了什么,才能真正理解 Solidity。

当您“调用智能合约”时,您正在向合约发送数据以及如何执行的说明。

数据编码有很多种,json、xml、protobufs等。Solidity 和以太坊使用 ABI 编码

这里我们不讨论 ABI 的规范。但您需要知道的是,它总是看起来像一个字节序列。

函数被标识为四个字节的序列。我们的原始字节序列(0x92d62db5) 其中有四个字节:92、d6、2d、b5。

请记住,一个字节是 8 位,8 位最多可以是 255 (2^8 - 1) 的值。以十六进制表示的字节可以从 0x00 到 0xff。将 0xff 转换为十进制,希望这可以清楚地表明这一点。

当函数不带参数时,发送代表该函数的四个字节指示智能合约执行该函数。

但如果采用参数,数据会是什么样子呢?

包含函数参数的solidity msg.data

 

我们得到

0xf8689fd30000000000000000000000000000000000000000000000000000000000000007

回到了我们身边。 f8689fd3 部分表示调用函数“takeOneArg”,带有大量前导零的 7 表示传递数字 7。

如果我们必须手动完成此操作,那将会非常混乱。

值得庆幸的是我们没有。

看这个。

使用 abi.encodeWithSignature 的 Solidity 函数

 

我们现在不需要关心 ABI 编码的规范,让我们轻松地使用它吧。

 

考虑以下示例。

如何在 remix ide 中方便地使用带有两个参数的 Solidity 函数。下拉菜单突出显示。

 

请注意,我们使用的是“abi.encode”和“abi.decode”。 “withSignature”位是指涉及函数时,但这里不是这种情况。

在此示例中,变量 x 和 y 被 abi 编码为

0x000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000 00000000f

十进制数字转换为十六进制,这就是为什么“5”仍然是“5”,但 15 变成了“f”。

如果我们事先知道这是一对 uint256,我们可以使用上面截图的函数将其“解码”回一对。

在 abi.decode 中显示为第二个参数的元组是有关如何解码数据的指令。如果您在此处提供了错误的数据类型或错误的元组长度,您将得到错误的结果,或者代码将恢复。

问题

编码

解码器

调用其他合约

 

到目前为止,我们所做的一切都是直接调用智能合约。但智能合约之间相互通信也是可能的,事实上也是可取的。

让我们举一个最简单的例子。

 

您可以通过以下方式查看它的实际效果

remix ide 显示了两个智能合约的使用

因为我们回顾了元组、abi 编码, 和字节内存 这里唯一令人惊讶的事情应该是称呼 事实是问生命的意义() 不是视图函数。

为什么askTheMeaningOfLife()不是一个视图函数?如果您尝试使用视图修饰符编译它,它将无法编译。

视图函数是只读的。当你调用任意智能合约的函数时,你无法知道它是否是只读的。因此,如果某个函数调用另一个智能合约,Solidity 不允许您将其指定为视图。

另外,虽然我们可以看到生命和一切存在的意义另一份合同 返回一个 uint256,一般情况下我们无法知道这一点。它可以返回一个字符串。

函数始终返回 abi 编码的字节。 remix 如何知道将字符串格式化为字符串,将数字格式化为数字?它在幕后做着abi解码 我们在这里所做的操作。

是什么布尔好的元组的一部分?对其他智能合约的函数调用可能会失败,例如,如果函数恢复。要了解外部调用是否已恢复,将返回一个布尔值。在此实现中,调用函数askTheMeaningOfLifeAndAllExistence 也会恢复,但这不一定是一般要求。

这是一些有趣的事情。如果调用不存在的智能合约会发生什么?

尝试以 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db 作为参数来 AskTheMeaningOfLife(address source) 。

不要偷懒,试试上面的代码吧!

它会恢复,但这并不是因为地址不存在,而是因为您尝试解码空数据。如果我们注释掉解码部分,那么当我们调用不存在的地址时,该函数将不再恢复。

当您在 remix 中打开交易下拉列表时,您会在此处看到恢复说明。

Calling other contracts

 

如果另一个合约接受参数怎么办?这是执行此操作的代码。

 

注意“add(uint256,uint256)”中不要有空格

准备好将其付诸实践了吗?

练习题

交叉合约

代币交换迷你项目

 

This is challenging

Believe it or not, you now have enough prerequisite knowledge to build a very simple token exchange smart contract! Here is your mission.

Build two ERC20 contracts: RareCoin and SkillsCoin (you can change the name if you like). Anyone can mint SkillsCoin, but the only way to obtain RareCoin is to send SkillsCoin to the RareCoin contract. You’ll need to remove the restriction that only the owner can mint SkillsCoin.

Here is the workflow

  • mint() SkillsCoin to yourself

  • SkillsCoin.approve(address rareCoinAddress, uint256 yourBalanceOfSkillsCoin) RareCoin to take coins from you.

  • RareCoin.trade() This will cause RareCoin to SkillsCoin.transferFrom(address you, address RareCoin, uint256 yourBalanceOfSkillsCoin) Remember, RareCoin can known its own address with address(this)

  • RareCoin.balanceOf(address you) should return the amount of coin you originally minted for SkillsCoin.

 

Remember ERC20 tokens(aka contract) can own other ERC20 tokens. So when you call RareCoin.trade(), it should call SkillsCoin.transferFrom and transfer your SkillsCoin to itself, I.e. address(this).

If you have the SkillsCoin address stored, it would look something like this

 

Deploy these contracts in Remix and test that they work.

If you are new to Solidity, set aside a couple days for this. A lot of engineers get weirded out by the fact that balances are stored in smart contracts, not wallets, so it takes a bit of time to get used to this. Also, trust me, you will get confused by the cross contract calls.

应付功能

 

到目前为止,我们一直在使用代币来代表价值,但是以太币呢?我们来介绍一下智能合约如何与以太币交互。

 

After you deploy this contract on Remix, you’ll note that the button for interacting with payMe turns Red. This means you can specify in the field above how much value (Ether) to send when you call the function.

将以太坊发送到 remix ide 中智能合约中的应付函数

 

您可以以 Wei、Gwei、Finney 或 Ether 为单位发送 Ether。

1 Wei 是 1/10^18 以太币,1 gwei 是以太币的十亿分之一,1 Finney 是十分之一。

让我们把这个简单化,只发送一个以太币。

当我们点击“howMuchEtherIHave”时,我们实际上返回了

1000000000000000000

这并不意味着我们凭空创造了大量的以太币。请记住,浮点数不是区块链上的东西,因此 Ether 使用与 ERC20 代币相同的小数策略。一个以太币的单位实际上是 1 Wei,而我们传统上认为的 1 以太币是 10^18 Wei。

顺便说一下,.balance 结构适用于任意地址。智能合约可以通过以下功能来确定你的富有程度

 

10000000000000000000

Unless functions have the payable modifier, they will revert if they receive Ether.

Why have this construction? If someone wants to send us Ether, why not accept it?

This has been a subject of debate, but the general idea is that a function should constrained in such a way to have extremely well defined behavior. Anything outside of that should be restricted. The more constrained the behavior, the easier it is to reason about the smart contract’s functionality.

By the way, solidity provides a very convenient keyword for dealing with all the zeros involved with Ether. Both of these functions do the same thing, but one is more readable.

 

It is also valid to make a constructor payable, if you want your smart contract to begin life with privilege and a headstart. But you still need to explicitly send ether at construction time.

Just because a function is payable does not mean that the person calling the function has to send Ether.

Sending Ether

It’s clear how to send Ether if you initiate the transaction from Remix, but what if another smart contract wants to send Ether?

You will use the **call function we described earlier, but with an extra “meta argument.” It may look strange at first, but you’ll get used to it.]

 

让我们分解一下我们在这里看到的内容。

  • call 在 call 和参数之间有一个看起来很有趣的类似 json 的对象。这就是通过调用发送以太币的方式。 “value”键决定发送的金额。默认情况下为零

  • 我们有一个第二个参数为空的元组; (布尔好吧,)。这意味着我们忽略了 takeMoney() 的返回值。如果我们不关心返回值,我们就使用这种结构。

  • 我们仍然关心转移是否失败。所以我们已经完成了 require(ok) 构建。

 

一些实验

  • 部署这两个合约,但在构建时向 SendMoney 提供以太币。在调用 sendMoney 之前和之后查看两个合约上的 myBalance

  • 删除 takeMoney 上的“应付”修饰符,看看会发生什么(它应该恢复)

  • 调用 sendMoney 时使用以太币,并注意接收以太币余额如何增加

应付函数不能是视图函数或纯函数

改变智能合约的以太币余额是区块链上的“状态改变”。这是一种永久性的更改,即使在事务完成后也会持续存在,类似于更新存储变量。因此,应付功能不能是视图或纯粹的。编译器不会接受这一点。

Practice Problems

PriceIsRight

收到

仅仅为了发送以太币就必须对函数进行 abi 编码,这有点烦人。幸运的是,Solidity 有一个很好的方法来处理这个问题。

 

请注意一些新的事情:

  • receive 是一个函数,但它没有 function 关键字。这是因为它是一个像构造函数一样的“特殊”函数,所以强调一下,不包括“function”关键字。

  • 我们使用了修改器外部的 而不是民众

 

到目前为止,只要我们希望函数可以在合约之外调用,我们就一直使用 public 修饰符。外部意味着可以仅有的 在合同之外被调用。为了简单起见,我们将使用民众 但稍后我们将详细讨论外部和公共之间的区别。然而,Solidity 只允许收到 函数为外部函数。

也必须是付费的。尝试删除 payable 关键字并编译合同。那不会成功。

现在,另一个函数如何向它发送以太币?

 

现在将其插入混音中进行测试。

这干净了很多。没有abi编码什么的。

这种结构也是我们汇款的方式钱包。这是一份只允许一个地址提取以太币的合约。

 

我们正在对没有功能的钱包进行“功能调用”,这可能看起来很奇怪。

该约定有点令人困惑。只要记住这一点。

所有调用都传输以太币。但零是可传输的有效以太币数量。

在以太坊中,即使没有显式设置 value 参数,所有函数调用都可以传输以太币。然而,零是要传输的有效以太币数量,并且在不需要传输以太币的情况下很有用。

区块时间戳和区块编号

 

We’ve been able to do some cool stuff up to this point, but we haven’t been able to track the passage of time.

You can get the unix timestamp on the block with the block.timestamp. Let’s try it

 

Try it out in Remix.

The number that comes back is the number of seconds since January 1, 1970 UTC, the traditional unix time. Remember, this is seconds not miliseconds as your linux desktop or other programming languages might respond with.

Ethereum progresses with blocks, and whichever timestamp you get back is what the validator put into the block when they produced it. Since blocks are produced every 12 seconds, the block.timestamp will roughly increment by that amount. You shouldn’t trust block.timestamp on the order of second intervals. There’s too much variation. Over the course of minutes however, it is quite reliable.

If you want to ensure someone doesn’t call a function more than once per day, you can use the following construction

 

Solidity has a much nicer way to represent time instead of multiplying seconds like that.

 

In fact, seconds, minutes, hours, days, and weeks are all valid units of time, which are just handy shortcuts for multiplying out the number of seconds you need. In case you were wondering, seconds doesn’t change the value, but it does provide readability if you intend to use seconds as a measure.

block.number

You can also know what block number you are on with this variable. Hopefully it’s obvious what it does. Some people mistakenly multiply the average blocktime by the block.number to measure the passage of time. Don’t do that.

Don’t use block.number to track time, only to enforce ordering of transactions.

When do you need to enforce transaction order? Not often. So if you aren’t sure, use block.timestamp.

Etherscan shows the current blocknumber, if you want to have an idea of how large it currently is.

etherscan.io 的屏幕截图显示当前区块编号

 

上面的代码将告诉您交易发生在哪个区块。在这种情况下,它将动态更新。

如果您想强制一个函数在另一个函数之后调用,即在后面的块中调用,您可以使用以下构造。

 

Technically, our “ERC20” token is not fully ERC20 compliant. It’s missing an important feature: events.

General rule of thumb: If a function causes a state change, it should be logged.

Why log things? Isn’t it the case that the blockchain already immutably stores every transaction?

This is true, events are not strictly necessary. However, they make auditing past events a lot easier. Rather than combing through a bunch of transactions, the user can filter by the log that they care about and quickly find events (transactions) that might be of interest.

This is how your cryptocurrency wallet can quickly discover your ERC20 balance. It would be pretty annoying to have to look through every transaction that ever occurred on an ERC20 token to discover if you own any. But logs are stored in such a way that this retrieval is efficient.

Events cannot be seen by other smart contracts. They are optimized for being queried offchain.

Let’s look at an example.

 

一个事件最多可以有 3 个索引类型,但对未索引参数的数量没有严格限制。

如果您有数据库背景,您可以像对待数据库索引一样思考“索引”。

顺便说一句,数据类型后面的参数名称是可选的。我们可以将上面的事件写为

 

with no I’ll effects, except that perhaps it is a bit less readable.

When should a variable be indexed or not? If you might be interested in finding that value quickly, like “has an address been involved with this token contract” then you should index it. You probably are not interested in the question “has anyone ever transfered exactly 1,370,904 tokens in this contract, so don’t index the amount. Here is our ERC20 token with the events added. Note that these events are required by the specification.

Pay close attention to where the events have been added, especially the mint function! The convention of address(0) being the source means the tokens came into existence out of nothing, rather than from another address. Recommended reading: https://www.rareskills.io/post/ethereum-events

 

Practice Problems

Emitter

Emitting Events

遗产

 

Implementing an ERC20 contract from scratch each time would no doubt get tiring. Solidity behaves like an object oriented language and allows for inheritance. Here is a minimal example.

 

部署到 Remix,但从下拉列表中选择要部署的子项,而不是父项。

remix ide 屏幕截图展示了 Solidity 中的继承

 

尽管Child是空的,但我们在Child中看到了该函数

在使用继承创建的 remix ide 中测试智能合约

When a “contract” is “another contract”, it inherits all it’s functionality.

Like other object oriented programming languages, functions can be overriden. Here is the construction for changing the value.

Remix IDE 的屏幕截图,展示了 Solidity 中的虚拟函数

Note that only virtual functions can be overriden. If you try to override a function that isn’t virtual, the code won’t compile.

Also, when a function overrides, it must match exactly, both in name, arguments, and return type.

 

Solidity supports multiple inheritance

 

如果您想知道,如果两个父母有一个同名的函数,孩子必须覆盖它,否则行为将不明确。如果您最终遇到这种情况,则可能是您的软件设计出了问题。所以我们不要走那条路。

私人与内部

有两种方法可以使函数无法从外部访问:给它们一个私有或内部修饰符。区别很简单。

子合约无法“看到”私有函数(和变量)。

内部函数和变量都可以。

超级关键字

super 关键字的意思是“调用父函数”。以下是它的用途

 

If we didn’t include the super keyword here, foo() would call itself and go into infinite recursion. Try removing super and running the code in Remix. The transaction will revert because of the infinite recursion (Ethereum doesn’t let code run forever, it forcibly terminates them. The exact mechanism is an intermediate topic for later discussion).

Super means “call the parent’s foo, not mine.” This let’s us get all the functionality of foo without having to copy and paste the code.

Calling the parent’s constructor

Solidity won’t let you inherit from a parent contract without initializing it’s constructor. Consider this situation.

 

修复方法是在继承时调用父构造函数

让我们总结一下我们所学到的知识

  • 只能重写虚函数

  • 覆盖父函数的函数必须具有 override 修饰符

  • 重写函数的名称、参数和返回类型必须完全匹配

  • 您可以使用 super 关键字,而不是复制和粘贴父函数的代码

  • 您可以继承多个合约

  • 进行继承时,您必须显式调用父级的构造函数。

轻松创建 ERC20 代币

继承与 import 语句相结合,使我们可以轻松地利用其他人创建的库。在Remix中部署这个合约,你会看到所有的ERC20功能都已经为你实现了。

remix ide 的屏幕截图显示了继承自 OpenZeppelin erc20 智能合约的空合约

澄清一点

作为实体对象的智能合约与部署在区块链上的智能合约之间存在着天壤之别。

您无法继承部署在区块链上的合约。

它们是生活在你体外的二进制斑点。由于术语含糊不清,一些 Solidity 开发人员担心函数和变量可能被恶意合约继承和覆盖。这不可能发生。尽管我们将部署的代码称为“合约”,将 Solidity 代码称为“合约”,但它们并不是同一回事。

接口

 

I’m sure you thought the way we were calling other contracts with .call and abi.encodeWithSignature was a bit clumsy.

I wanted you to go through that exercise so that you know what is happening under the hood.

Now it’s time to introduce the ergonomic way Solidity enables cross contract calls.

 

V1 和 V2 非常相似,但它们在本质上有一个关键的区别。我们稍后会谈到这一点。

这里重要的事实是 V2 比 V1 干净得多!

该接口很好地为我们封装了abi编码和解码,这样我们就不用考虑它了。该接口定义了返回类型,它定义了 abi 解码的工作方式,函数签名和参数定义了 abi 编码。

现在我们可以将其全部抽象出来并调用另一个合约函数,就好像它只是另一个函数调用一样。

很酷吧?

请注意,接口函数的修饰符是外部的 不公开。您不能将接口声明为公共的,只能声明为外部的。您无需声明内部函数,因为外部合约对此一无所知。

到目前为止,除非绝对必要,否则我一直通过公开所有内容来简化事情。但自从我们学习了二十多章以来,作为一名 Solidity 开发者,你已经成长了。因此,我现在将介绍您应该遵循的最佳实践。

除非需要从智能合约内部调用函数,否则它应该是外部的,而不是公共的。

接口不允许您将函数声明为公共函数,因为如果您正在交互的函数可以从其他智能合约中调用,那么它与外部无关。

现在让我们看看另一个关键区别。

getSum 是 V1 中的常规公共函数,但它是看法 V2 中的函数。

等等,我们不是不能这样做吗,因为我们不知道其他函数是否会修改状态?

Solidity 有一种特殊的调用,静态调用 其行为类似于常规调用,但如果发生状态更改则强制事务恢复。

这是有效的坚固性。

 

我知道您害怕 ABI 编码的东西,但我想向您展示当接口声明要查看的函数时幕后会发生什么。这意味着底层操作将通过静态调用发生。

鼓励读者在 add 内部进行状态更改,以查看事务恢复。

如果您有 Java 背景,那么接口的整个部分可能是相当明显的。但重要的是要记住,在幕后,正在发生使用 abi 编码的跨合约调用。您并不是将另一个智能合约“编译”为您自己的智能合约,就像 Java 对象如何组合在一起一样。

修饰符

 

The onlyOwner construction is so common that we’ll dedicate a section to it.

Consider the following

 

让我们来看看这个节目的明星:

 

It simply means “execute the code before the underscore, then execute the function.”

This is a handy way to “modify” the function behavior, hence the name “modifiers.”

Note that even though HoldFunds inherited from Ownable, it didn’t override any functions. Inheritance in Solidity is more often a mechanism for including behavior than for defining some kind of polymorphism (don’t worry if you don’t know what that is).

So in this case, if you want your smart contract to have nice handy functions inside of it, you can import another contract that provides the functionality you need.

It isn’t strictly necessary. You can put all your code into one big contract. But that code would be less readable.

Modifiers can be used for things other than checking ownership, but ownership checks are the most common use case.

Don’t modify state inside modifiers. Although solidity allows you to do this, it makes the code harder to reason about. This is considered bad practice.

Immutable variables

 

如果您永远不会更改变量,最好明确说明您的意图。 Solidity 有一个关键字。

 

If a variable is set in the constructor and never updated, it should be immutable

If you try to write to an immutable variable, the code will not compile.

 

Practice Problems

Immutable

常数

 

Immutable variables can be set once in the constructor, but what if you have a number that you never want to change?

Like other languages, Solidity has the constant keyword. This signifies that the value is fixed an never changes.

Let’s say you have an ERC20 token that should never have more than 22 million minted.

This would be the clean way to do it

 

请注意,22000000 写为 22_000_000。它们的意思是相同的,但后者更具可读性。数字中的下划线将被忽略。

将 ERC721 部署到 OpenSea

 

An ERC721 is very similar to an ERC20. It has an notion of transfering, balance, and allowance. The only difference is that each token has a unique ID, and there is only one of them.

We’ve been throwing a lot of information at you with calling contracts and sending ether. We’ll build an ERC721 to reinforce everything we learned earlier.

Now that we know how to transfer Ether, you have enough knowledge to sell NFTs for Ether, like a traditional NFT sale.

This tutorial is in video form.

https://www.youtube.com/watch?v=LIoFbudNVZs

使用 Foundry 启动并验证 NFT

In the video tutorial above, you put an NFT on Opensea using the Remix environment.

This is fine, but Remix is not ideal for production use.

In this chapter, we will show you how to

  1. Set up Foundry with the NFT

  2. Deploy it to the Sepolia testnet and verify it on Etherscan

 

If you’ve been doing the exercises, you should already have foundry installed, so let’s set it up.

 

Create a new folder; call it foundry-nft, cd to it, and run “forge init” in the empty folder.

Copy and Paste the code

Here’s the code for creating the NFT. Rename the Counter.sol file in the “src” directory to FoundryNFT.sol and paste this code.

Installing Openzeppelin

We import and inherit the Openzepplin ERC721 contract, so to install it use the following:

Using remappings

Coming from a Remix background, you’ll notice that the OpenZeppelin import path here differs. Remix doesn’t store libraries in the same location Foundry does. However, Foundry provides remappings to allow file lookups on import to be redirected to a different location.

To see all the available remappings, we run forge remappings.

We get this:

 

我们可以看到 OpenZepplin 重新映射,因此我们不需要指定 OpenZepplin 合约的完整路径。我们可以用openzeppelin-合约/合约/代币/ERC721/ERC721.sol 而不是首先指定“lib/”目录。

不使用重新映射

如果我们不使用伪造重映射,则必须指定文件或目录的完整路径。

例如,对于 ERC721.sol 文件,我们将执行类似的操作来导入它。

 

To confirm this is still valid, run forge build and see if it compiles.

 

And it does

 

现在撤消更改,恢复到原来的状态。

一步生成remappings.txt文件

这可以通过以下操作来完成

 

 

Change the file names

 

We can further configure where the remappings point by creating a remappings.txt file in the project root directory and adding this line @openzeppelin=lib/openzeppelin-contracts/contracts to the file.

After doing this, we can use import "@openzeppelin/token/ERC721/ERC721.sol"; to import the ERC721 token instead of explicitly writing the full file path.

This remapping can be configured in the remappings.txt file if it points to the right path.

Getting an Etherscan key

When we deploy our NFT, we need an Etherscan API to verify the contract. This will let us easily connect to Etherscan to verify the smart contract using forge without going to the Etherscan website and following the process.

Head to Etherscan, log in, and create an API key.

Etherscan 主屏幕
Etherscan API 密钥

 

We have created U3D9IS6Z5E872VFS7M7AWR1SBA8786ZZ3Y as our API key. We’ll be using this API key later.

 

 

Creating a throwaway wallet

Never use the private keys to a hardware wallet or any wallet that holds significant value.

 

To deploy the NFT contract to the testnet, we need a private key with test ether to sign the transaction.

 

For the sake of simplicity, we have created a throwaway wallet and funded it with some sepolia ether.

 

Here’s the private key of the wallet; 787ea4ec95ab4f4e66c4c4c387cd0b5fbbec84a9293db485fa5f86f490c157d4.

(This shouldn’t be used as it is considered comprised already.)

 

 

Put the Etherscan key and private key of the wallet in your environment variables

Now that we have both our API key and private key, the next step is to create a .env file in the project’s root directory and add this to it.

Make sure your .env file is included in your .gitignore so you don’t accidentally publish your private keys!

Run this script

Run this script to deploy and verify the NFT contract on sepolia.

 

在 Etherscan 上查看

 

我们已经成功部署了sepolia 测试网上的 NFT 合约。

Units of Ethereum: wei, gwei, and ether

 

下面两行代码是等价的

 

If you feel like counting, that’s 10^18. Remember, there aren’t floats in Solidity, so “1 Ethereum” is actually 10^18 units of its smallest unit.

The smallest unit of Ether is 1 wei. 10^18 wei is 1 Ether.

 

Another commonly used unit is gwei. One gwei is 1 billion wei, or 1 billionth of an Ether.

 

Remix doesn’t let you specify a fraction of an Ether when sending value, so you’ll have to calculate the amount from the fraction and convert it to wei or gwei.

 

By the way, even though floats are not supported in Solidity, you can specify fractions of an Ether. The solidity compiler is smart enough to understand that a fraction of an Ether isn’t a fraction itself. The following lines of code are equilvalent

 

顺便说一句,以太币还有其他单位名称,例如芬尼(Finney)和萨兹博(Sazbo),但这些单位很少使用,当你需要知道它们的价值时,最好只用谷歌搜索它们。但是必须记住以下值:

  • wei 是以太币的最小单位。

  • 10^18 wei 是 1 个以太币

  • 1 gwei 是十亿 wei,或者说是以太币的十亿分之一。

结构体

Solidity 中的结构的行为与 C 类似。它们将不同的变量分组到单个复合数据类型中,这对于组织数据和创建更复杂的数据结构非常有用。

以下是在 Solidity 中声明结构的方法。

 

myFoo is a public variable of struct Foo, it stores both uint256 a and uint256 b. As you can see if we deploy it in remix, myFoo returns:

myfoo.png

要将 Remix 中的结构传递给以结构作为参数的函数(稍后我们将详细讨论这一点),请按如下方式对其进行编码:

encode

The function in question takes the example Foo above, which consists of two uint256 variables. It might be a bit confusing to format it like an array, but that’s how it works.

To create a new instance of Foo in Solidity, simply wrap the values in struct Foo.

  • Foo( a , b )

To access or assign each individual variable in the struct myFoo , use the dot notation.​

  • myFoo.a

  • myFoo.b

Why do we use structs? Let’s say we have a deposit contract that keeps track of the depositor’s name and balance.

In the contract above, the depositor’s name and balance is stored into two separate mapping data structure.

The address variable in the mapping is repeated twice for both the name and balance of the same msg.sender, and hence it is not efficient.

So here’s where structs come in handy, we can register both the name and balance under a struct variable, and store that variable in one key value pair mapping like this.

看看它有多有用吗?它使您的代码更干净、更高效。

如何使用结构体

简单吧?这是演示。

如果您想将 struct Foo 作为参数或返回值传递,则必须遵循以下规则:

  • 作为参数传递的结构必须声明为内存并且

  • 作为返回类型的结构也必须声明为内存。它看起来是这样的。

需要注意的是,Solidity 中的结构体不能包含其自己类型的成员。例如,这是不允许的

Arrays and mappings

Structs can be used as the value type in arrays and mappings. For example, you could create a dynamic array of the Foo instance like this:

arrayFoo is an array composed of Foo instances.

To demonstrate:

You could also create a mapping where the keys are addresses and the values are Foo instances:

Up to this point, it should be obvious what’s going on here. We have a mapping of address ⇒ struct Foo; mappingFoo.

To assign a Foo instance to an address mapping, here’s how we do it

并修改它

Real Life Example

A more practical use case would be in a ticket system. We have a BuyTickets contract that sells one ticket at a price of 0.01 ether. An address can’t purchase more than 10 Tickets and we have a function that displays an address’ ticket information.

我们可以使用 NFT 来获取门票,但如果用户不打算互相转让门票,那就有点矫枉过正了。

练习

学生数据库

弦乐

 

字符串是动态大小的 UTF-8 编码字节,也是动态大小的字节数组。两者可以互换,只需分别使用 string() 将字符串转换为字节和 bytes() 将字节转换为字符串。这极大地帮助我们像使用其他编程语言一样对字符串进行操作。然而,由于字符串是 UTF-8 编码的,如果此类字符需要超过 1 个字节,则会增加字符串操作的难度。

因为字符串是数组,所以它们在传递给函数时需要 calldata 或内存修饰符,并且在返回时需要内存修饰符。

检查字符串的长度:

 

这并不意味着字符串中有多少个字符,而是意味着字节数组有多长。 Unicode 字符占用多个字节。

要访问字符串的字符:

 

请记住,只有整个字符串都是 ascii 时,这才有效。如果我们传入占用超过一个字节的 unicode 字符,例如“你好”,代码就会崩溃。

从字符串中获取字符比像 javascript 或 python 那样对其进行索引要困难一些,因为我们必须初始化一个长度为 1 的字符串数组,然后将我们想要获取的字符插入到新的字符串数组中。这就是上面的代码所做的事情。

Solidity 支持 unicode 字符串:

 

有点误导的是,如果使用十六进制修饰符,我们使用“字符串”来表示十六进制数据。下面的鞋子将“helloworld”的十六进制编码转换为 helloworld。

 

在 Solidity 0.8.12 中,通过添加 string.concat(),连接字符串变得更加容易。低于该版本, string.concat() 不可用。

Unit Testing in Solidity

It's about time you learned how to write unit tests! We've already written a tutorial on unit testing, so no need to repeat it on this page

Read it here: Solidity Unit Testing (please link "Solidity Unit Testing" here: https://www.rareskills.io/post/foundry-testing-solidity)

Homework:

  • Unit test your NFT. Make sure that when you mint, the ownerOf the NFT is the address that minted it. Also check that "balanceOf" for that address becomes 1.

  • Check that the balance of the contract went up by the price of the NFT

  • When the owner calls withdraw, check that their Ether balance went up by the expected amount

bottom of page