目录
[1.1 核心特色与功能](#1.1 核心特色与功能)
[1.2 如何参与并获得收益](#1.2 如何参与并获得收益)
[1.3 生态系统与发展](#1.3 生态系统与发展)
[1.4 重要风险提示](#1.4 重要风险提示)
[3.1 介绍](#3.1 介绍)
[3.2 资源](#3.2 资源)
[3.3 学习最佳实践](#3.3 学习最佳实践)
[3.4 自我测试](#3.4 自我测试)
[3.5 结论](#3.5 结论)
[4.1 介绍](#4.1 介绍)
[4.2 存储库使用情况](#4.2 存储库使用情况)
[4.3 提问](#4.3 提问)
[4.4 设置](#4.4 设置)
[4.5 结论](#4.5 结论)
[4.6 测试一下自己](#4.6 测试一下自己)
[5.1 介绍](#5.1 介绍)
[5.2 Remix IDE](#5.2 Remix IDE)
[5.2.1 Remix IDE 的核心特点](#5.2.1 Remix IDE 的核心特点)
[5.2.2 主要功能模块](#5.2.2 主要功能模块)
[5.2.3 Remix IDE vs. 本地 IDE(如 VSCode)](#5.2.3 Remix IDE vs. 本地 IDE(如 VSCode))
[5.2.4 最适合的使用场景](#5.2.4 最适合的使用场景)
[5.2.5 如何开始使用?](#5.2.5 如何开始使用?)
[5.2.6 一些实用技巧](#5.2.6 一些实用技巧)
[5.3 编译器指令](#5.3 编译器指令)
[5.4 SPDX 许可证标识符](#5.4 SPDX 许可证标识符)
[5.5 编写智能合约](#5.5 编写智能合约)
[5.6 编译](#5.6 编译)
[5.7 结论](#5.7 结论)
[5.8 测试一下自己](#5.8 测试一下自己)
[7.1 数据类型](#7.1 数据类型)
[7.1.1 变量定义](#7.1.1 变量定义)
[7.1.2 字节和字符串](#7.1.2 字节和字符串)
[7.2 合约逻辑](#7.2 合约逻辑)
[7.3 结论](#7.3 结论)
[7.4 测试一下自己:](#7.4 测试一下自己:)
[8.1 介绍](#8.1 介绍)
[8.2 创建存储函数](#8.2 创建存储函数)
[8.3 部署智能合约](#8.3 部署智能合约)
[8.4 创建交易](#8.4 创建交易)
[8.5 变量的作用域](#8.5 变量的作用域)
[8.6 结论](#8.6 结论)
[8.7 测试一下自己](#8.7 测试一下自己)
[9.1 介绍](#9.1 介绍)
[9.2 数组和结构体](#9.2 数组和结构体)
[9.2 结构体数组](#9.2 结构体数组)
[9.3 结论](#9.3 结论)
[9.4 测试一下自己](#9.4 测试一下自己)
[10.1 介绍](#10.1 介绍)
[10.2 错误和警告](#10.2 错误和警告)
[10.3 充分利用你的资源](#10.3 充分利用你的资源)
[10.4 Phind](#10.4 Phind)
[10.5 其他资源](#10.5 其他资源)
[10.6 结论](#10.6 结论)
[10.7 测试一下自己](#10.7 测试一下自己)
[11.1 介绍](#11.1 介绍)
[11.2 数据位置](#11.2 数据位置)
[11.3 调用数据和内存](#11.3 调用数据和内存)
[11.3.1 calldata 和 memory介绍](#11.3.1 calldata 和 memory介绍)
[11.3.1 调用数据(calldata)](#11.3.1 调用数据(calldata))
[11.4 存储storage](#11.4 存储storage)
[11.5 字符串和基本类型](#11.5 字符串和基本类型)
[11.6 结论](#11.6 结论)
[11.7 测试一下自己](#11.7 测试一下自己)
[12.1 介绍](#12.1 介绍)
[12.2 避免代价高昂的迭代](#12.2 避免代价高昂的迭代)
[12.3 Mapping(映射)](#12.3 Mapping(映射))
[12.4 结论](#12.4 结论)
[12.5 测试一下自己](#12.5 测试一下自己)
[13.1 介绍](#13.1 介绍)
[13.2 在测试网上部署](#13.2 在测试网上部署)
[13.3 合同互动](#13.3 合同互动)
[13.4 结论](#13.4 结论)
[13.5 测试一下自己](#13.5 测试一下自己)
[15.1 介绍](#15.1 介绍)
[15.2 测试网资金](#15.2 测试网资金)
[15.3 部署](#15.3 部署)
[16.1 介绍](#16.1 介绍)
[16.2 钱包连接](#16.2 钱包连接)
[16.3 Bridging Sepolia](#16.3 Bridging Sepolia)
[16.4 资金转移](#16.4 资金转移)
[17.1 介绍](#17.1 介绍)
[17.2 编译](#17.2 编译)
[17.3 部署](#17.3 部署)
[17.4 验证部署](#17.4 验证部署)
[17.5 检查部署情况](#17.5 检查部署情况)
[17.6 结论](#17.6 结论)
[18.1 zkSync Remix 插件的小错误](#18.1 zkSync Remix 插件的小错误)
[21.1 介绍](#21.1 介绍)
[21.2 EVM](#21.2 EVM)
[21.3 Contract Setup](#21.3 Contract Setup)
[21.4 类型和结构](#21.4 类型和结构)
[21.5 功能和行为](#21.5 功能和行为)
[21.6 数据位置和内存](#21.6 数据位置和内存)
[21.7 结论](#21.7 结论)
[21.8 测试一下自己](#21.8 测试一下自己)
第01节:认识gmx
GMX 是一个领先的去中心化永续合约交易所,允许用户直接从自己的加密货币钱包进行高达 100 倍杠杆的交易,自 2021 年开始运营。

**备注:**了解更多关于 GMX 的信息,请访问 https://gmx.io。
1.1 核心特色与功能
GMX 的设计旨在提供与传统中心化平台不同的交易体验:
-
去中心化与无许可:无需注册、无需 KYC(了解你的客户),连接自托管钱包(如 MetaMask)即可开始交易。你的资产始终由自己掌控,无需存入交易所。
-
独特的交易模式:它不依赖订单簿,而是使用一个动态平衡的**流动性池(GLP)**来执行交易。这种模式可以提供更深厚的流动性,支持大额交易(网站称可超 5000 万美元)并减少滑点。
-
降低清算风险:采用 Chainlink 等提供的去中心化、高速更新的预言机价格数据,旨在避免因市场短期价格波动(价格毛刺)而导致的不公平清算。
-
多资产支持:用户可以使用多种主流代币(如 WBTC、ETH、USDC)作为保证金开立仓位。
1.2 如何参与并获得收益
在 GMX 生态中,主要有两种参与角色:
(1)作为交易者:支付交易手续费,这些费用会分配给流动性提供者。可以使用"一键交易"等功能来提升体验。
(2)作为流动性提供者(LP) :通过向 GLP 池子存入资产来提供流动性。作为回报,LP 可以赚取平台产生的 63% 的交易和清算费用,网站数据显示部分池子历史年化收益率可达约 35%。此外,持有 GMX 代币可以质押以获得奖励和治理投票权。
1.3 生态系统与发展
GMX 已经发展为一个庞大的 DeFi 基础设施:
-
多链部署:主要部署在 Arbitrum 和 Avalanche 上,并已支持 Solana、Botanix,并计划通过 LayerZero 扩展至更多区块链。
-
广泛的集成:已被数百个 DeFi 协议集成,如 Pendle、Radiant 等,成为重要的流动性基础层。开发者也可以利用其智能合约和 API 进行构建。
-
活跃的社区与数据:拥有庞大的用户群(网站显示超 73.9 万交易者)和可观的交易量(单日交易量曾超 10 亿美元),并由活跃的 Discord、Twitter(X)社区驱动。
1.4 重要风险提示
高收益伴随高风险。参与永续合约交易或提供流动性需要特别注意:
-
交易风险:高杠杆交易可能带来远超本金的巨额亏损。
-
流动性提供风险:作为 GLP 持有者,你的资产将承担交易对手方风险,即平台交易者的整体盈亏会影响池子资产价值。
-
智能合约风险:尽管经过审计并广泛使用,但去中心化协议始终存在潜在的安全漏洞风险。
-
市场风险:加密货币市场本身波动剧烈。
如果你对杠杆交易或成为流动性提供者感兴趣,建议你访问其官网(gmx.io)连接测试网深入体验,并务必在投入真实资产前,在其官方文档和社区中充分学习、理解所有机制和风险。
第02节:欢迎来到Solidity基础知识
Solidity基础全面指南------本课程将带您探索Solidity的世界,它是目前最主流的智能合约编程语言。学习如何使用Solidity进行编码和开发,从而成为智能合约开发人员、安全研究员,或以技术人员的...
第03节:最佳实践
3.1 介绍
欢迎来到Cyfrin Updraft平台。该平台结合了视频课程、文字内容和互动讨论,提供全面的学习体验。
3.2 资源
在课程视图的右上角,您会找到指向++GitHub 资源++ 页面的链接📂。该指南包含了课程所需的所有代码、信息和材料。在 GitHub 上,您可以通过++"讨论"选项卡++💬与同学和社区互动,提出问题、分享见解并协作解决问题。
视频课程旁边还有一个文字课程标签页📝。该标签页以文本格式提供课程内容,方便您边看边学、复制粘贴代码片段或查看更新内容。
如果您在 YouTube 上观看,视频描述中会提供这些资源的链接。不过,在 Cyfrin Updraft 上观看本课程还能获得更多优势,例如跟踪学习进度和访问文字版课程,这将提升您的学习体验。
3.3 学习最佳实践
(1)🌐通过使用 GitHub 讨论区和 Discord 进行实时交流,积极参与社区互动。记住,提问和与他人互动能够极大地加深你对学习内容的理解和记忆。
(2)🏝️定期休息:避免一次性吸收所有信息,因为你的大脑需要时间来处理和吸收知识。
(3)⏩调整视频播放速度:使用视频播放器中的速度控制功能,使课程进度与您的学习速度相匹配。
(4)📝如有需要,请使用字幕:如果英语不是您的母语,请利用视频设置中提供的字幕选项。
(5)🔄采用模块化教学:本课程设计灵活,您可以根据自己的兴趣和需求在不同主题间自由切换。您可以随时回顾已学课程,巩固知识,确保透彻理解。
(6)🎯利用测验和编程挑战 :Cyfrin Updraft 提供测验 和编程挑战。每个章节末尾都有测验,每个 GitHub 代码库章节末尾都有编程挑战。完成这些挑战后,您可以获得 NFT 作为荣誉徽章。
(7)❓学会提出格式良好的问题:提问也是一项技能。在论坛和讨论中提出清晰简洁的问题时,请记住这一点,以便从他人那里获得最佳帮助。
3.4 自我测试
📕 请提供至少 5 条建议的最佳实践,供大家在本课程中遵循。
3.5 结论
遵循这些指导原则,您就能更好地利用这门课程。积极参与社区活动、合理安排学习进度并充分利用 Cyfrin Updraft 的资源,将显著提升您的学习体验。
第04节:引言
4.1 介绍
首先,请访问Cyfrin Updraft 的官方 ++GitHub 代码库。++
👀❗重要提示: 每个课程都会有一个关联链接,您可以在其中找到每节课中要使用的所有代码以及一个 README 部分,其中包含有关如何使用代码的说明。
首次访问时,界面可能略有不同。您要查找的是与本课程相关的代码仓库。该仓库包含本阶段课程所需的所有代码,以及一个README章节。该章节README将为您提供大量关于如何使用代码的说明。
4.2 存储库使用情况
该存储库主要有两个用途:
-
🚪轻松访问:每节课都可以轻松查阅和复制
-
🗣 👥讨论和交流:您可以与同学互动、提问并参与合作学习。
🔥注意: 要提出问题或基于特定存储库发起讨论,请使用Cyfrin Updraft - 职业发展路径 的++"讨论"选项卡++,而不是直接在存储库本身上创建问题。
4.3 提问
在您的旅程中,您很可能会遇到问题。建议您使用讨论选项卡内的++问答++版块。我们会指导您如何以最佳方式表述您的疑问和问题,以便您的问题更有可能获得社区、人工智能或论坛的解答。
4.4 设置
在开始编写代码之前,您必须能够访问所提供的代码库和学习资源。
强烈建议您在以下平台注册账号:
-
++GitHub++
-
++ChatGPT++(但请记住,它可能并不总是提供准确的信息)。
-
++Google Gemini++(谷歌的免费 GPT 替代方案,可以理解 YouTube 视频,进行摘要、数据提取和内容搜索)。
4.5 结论
现在到了激动人心的部分:构建并部署你的第一个智能合约。 我们将使用一个名为++Remix的工具,它是一个用于部署和与智能合约交互的集成开发环境 (IDE)。你可以通过此链接++访问它。
💡提示: 要充分利用本指南,最好的方法是跟着视频一起编写代码。建议您调整教程视频的播放速度,以匹配您的编码速度。记住,在学习新技能的过程中,重复练习至关重要。
完成下一课后,你将已经构建并部署了你的第一个智能合约到区块链上。让我们马上开始吧!
4.6 测试一下自己
每节课结束时,你都会看到一个*"自我测试"* 部分。这部分将帮助你巩固刚刚学习和编写代码的概念。测试题 包括理论 题(标有📕)和编程题(标有👨💻)。
💡提示:在继续学习下一课之前,请确保你真正理解了答案。
第05节:部署你的第一个合同
5.1 介绍
在本课中,你将学习 Remix 的基础知识,以及如何创建和编译你的第一份合同。
5.2 Remix IDE
Remix IDE 是 以太坊智能合约开发领域最著名、最便捷的在线集成开发环境 。对于刚接触 Solidity 的开发者和资深工程师来说,它都是一个不可或缺的工具。简单来说,它是一个功能强大且完全在浏览器中运行的 IDE,让你无需在本地安装任何复杂的编译器或开发环境,就能编写、编译、调试和部署智能合约。下面我将从核心特点、主要功能模块、适用场景以及如何开始使用这几个方面为你详细介绍。
5.2.1 Remix IDE 的核心特点
(1)开箱即用,完全在线 :只需一个浏览器(推荐 Chrome/Firefox)和网络连接,即可访问 remix.ethereum.org 开始开发。所有操作都在云端进行。
(2)强大的内置工具链:
-
编译器:集成多个版本的 Solidity 编译器,一键编译。
-
调试器:提供强大的交易调试功能,可以单步执行,查看变量状态,是理解合约执行和排查错误的利器。
-
部署环境:内置 JavaScript VM(模拟区块链)、连接本地节点(如 Ganache)、连接测试网/主网等多种选项。
(3)插件化架构:通过激活不同的插件(Plugin),可以扩展 Remix 的功能,例如静态代码分析、单元测试、与硬件钱包交互等。
(4)完美的学习工具:对于初学者,它是理解智能合约从编写到部署全流程的最佳工具,没有环境配置的困扰。
5.2.2 主要功能模块
Remix 的界面主要分为几个核心面板:
- 文件资源管理器 :创建、打开、管理和保存你的 Solidity 合约文件(
.sol)。可以连接本地文件系统或 GitHub Gist。

-
代码编辑器:支持 Solidity 语法高亮、自动补全和错误提示。
-
Solidity 编译器:
-
选择编译器版本。
-
点击"Compile"按钮即可编译。
-
设置编译选项(如优化、EVM版本)。
-
-
部署与交互:
-
环境选择:从"Deploy & run transactions"面板中,你可以选择:
-
JavaScript VM:浏览器内模拟的区块链,重启后状态重置,非常适合快速测试。 -
Injected Provider(如 MetaMask):连接你的钱包(例如 MetaMask),部署到真实的测试网(如 Sepolia)或主网。 -
Dev-本地网络:连接到你本地运行的 Hardhat 或 Ganache 节点。
-
-
合约交互 :部署后,Remix 会生成一个直观的UI界面,你可以直接点击按钮来调用合约的函数(无论是
view读函数还是需要发送交易的写函数),并查看结果。
-
-
调试器:这是 Remix 的杀手锏功能。在部署或调用合约后,你可以在交易日志中点击"Debug"按钮,进入调试面板,逐条指令(Opcode)地执行,观察堆栈、内存、存储状态的变化。
5.2.3 Remix IDE vs. 本地 IDE(如 VSCode)
为了帮你更好地选择,这里有一个简单的对比:
| 特性 | Remix IDE | VSCode(配置 Hardhat/Truffle) |
|---|---|---|
| 运行方式 | 在线(浏览器) | 本地(桌面应用) |
| 环境配置 | 无需安装,开箱即用 | 需要安装 Node.js, 编译器,插件,配置项目 |
| 调试功能 | 内置,强大直观 | 需要配置,依赖外部工具(如 Hardhat Console) |
| 项目管理 | 较简单,适合单文件或小项目 | 强大,适合大型、复杂项目 |
| 团队协作 | 可通过分享 Gist 链接 | 使用 Git 进行版本控制,标准工作流 |
| 部署灵活性 | 内置多种连接方式,简单直接 | 更灵活,可编写复杂部署脚本 |
| 学习曲线 | 极低,适合入门 | 较高,需要更多开发知识 |
5.2.4 最适合的使用场景
(1)智能合约入门学习:零门槛,是所有人的第一站。
(2)快速原型验证:有一个新想法,想立刻写几行代码测试其逻辑可行性。
(3)教学与演示:在分享时,可以实时展示代码编写、编译、部署和交互的全过程。
(4)合约调试与分析:即使主要使用本地环境开发,当遇到复杂Bug时,也常将合约复制到 Remix 中使用其强大的调试器进行分析。
5.2.5 如何开始使用?
(1)打开浏览器,访问 remix.ethereum.org。
在左侧"文件资源管理器"中,创建一个新文件,例如 SimpleStorage.sol。
(2)在编辑器中编写你的第一个合约(可以从官网的示例开始)。
(3)切换到"编译器"标签,点击"Compile HelloWorld.sol"。
(4)切换到"部署与交互"标签,在"ENVIRONMENT"中选择 JavaScript VM。
(5)点击"Deploy"按钮。合约将被部署到模拟的区块链上。
(6)在"已部署合约"区域,你会看到你的合约,点击其函数按钮即可与之交互。
5.2.6 一些实用技巧
-
使用
Cmd/Ctrl + S可以自动编译。 -
在调试时,关注"Storage"的变化,这是理解合约数据存储的关键。
-
可以安装"Solidity Static Analysis"等插件来检查代码安全问题。
备注:点击此链接 ++即可开始++
5.3 编译器指令
该 pragma 指令指定用于构建源文件的 Solidity 编译器 版本 。编译器遇到此行指令时,会将其版本与您在此处指定的版本进行比较。如果编译器版本不同,Remix 会自动根据您的指定进行相应调整。
您可以通过以下方式指定编译器版本:
1. 只使用一个版本
rust
pragma solidity 0.8.19; // use only version 0.8.19
2. 使用介于较低和较高范围内的版本
rust
// use versions between 0.8.19 and 0.9.0 (excluded)
pragma solidity ^0.8.19;
pragma solidity >=0.8.19 <0.9.0;
🗒️注意: 请记住在代码中添加注释,以便日后参考。

5.4 SPDX 许可证标识符
SPDX 许可证标识符是添加到智能合约等开源代码文件开头的一行标准化、机器可读的许可证声明 ,其核心作用是明确告知用户和开发者该代码在法律上可被使用的条款和条件 ,避免了许可证不明确所带来的法律风险;在 Solidity 中,从编译器版本 0.6.8 开始,每个源文件都必须包含此标识符 (如 // SPDX-License-Identifier: MIT),否则编译器会发出警告,常见的标识符包括宽松的 MIT、著佐权性质的 GPL-3.0 以及拒绝授权的 UNLICENSED,其标准格式由 SPDX(软件包数据交换)组织维护,以增强开源生态的合规性与互操作性。
建议(虽然并非强制要求)在智能合约的初始阶段添加 SPDX 许可证标识符。从法律角度来看,这有助于简化代码的许可和共享流程。
rust
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
MIT 许可证被认为是最宽松的许可证之一,它赋予任何人使用以下代码的自由,并且基本上可以按照他们认为合适的方式使用它。
**备注:**MySQL 与 PostgreSQL 在 SPDX 许可证上的核心不同在于:MySQL 社区版采用具有"传染性"的 GNU GPL v2 许可证(SPDX标识符:`GPL-2.0-only`),这意味着若你修改其代码并对外部分发,则必须开源你的修改部分;而 PostgreSQL 采用极其宽松的类 BSD/MIT 许可证(SPDX标识符:`PostgreSQL`),允许你将其以任何形式(包括闭源商业产品)自由使用、修改和分发,而无需开源自己的代码。这一根本区别使 PostgreSQL 在商业集成和云服务中具有更低的合规门槛与法律风险。
5.5 编写智能合约
你可以使用关键字 contract 来开始编写你的合约,其后紧跟一个名称,例如 SimpleStorage。所有位于大括号内的代码都将被视为该合约的一部分。
如果你熟悉面向对象编程语言,可以将 contract 理解为类似于 类 的概念。

第一个智能合约代码:
rust
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract SimpleStorage {
// First a contract
}
5.6 编译
在 Remix IDE 中,选择 Solidity 编译器。
选择与 Solidity 文件中指定的版本相匹配的编译器版本。
按下Compile按钮。

编译代码意味着将人类可读的代码转换为计算机可读的代码或字节码。
如果看到绿色对勾,则表示编译成功。如果出现任何错误,Remix 会指出错误所在,您可以据此进行调试。
5.7 结论
做得好!你刚刚用 Solidity 创建并编译了你的第一个智能合约。
5.8 测试一下自己
(1)IDE 是什么意思?Remix 的主要功能有哪些?
在线版的solidity集成开发环境,可以完成solidity代码的开发、调试以及部署等操作。
(2)pragma 关键字的作用是什么?
答案:标明编译器版本
(3)请解释"编译合约"意味着什么。
编译代码意味着将人类可读的代码转换为计算机可读的代码或字节码
(4)编写一个包含 SPDX 许可证标识符的空合约,该合约可使用 0.8.11 或 0.8.13 版本编译。
第06节:小测试



7.1 数据类型
Solidity 支持多种基本 类型,这些类型可以组合起来创建更复杂的类型。您可以在 ++Solidity 文档++中了解更多信息。
| 类别 | 类型名称 | 关键字 | 大小/说明 | 默认值 | 示例 |
|---|---|---|---|---|---|
| 布尔型 | 布尔值 | bool |
1字节 | false |
bool isActive = true; |
| 整型 | 有符号整型 | int8 ~ int256 |
8-256位(步长为8) | 0 |
int8 x = -10; int256 large = 1000; |
| 整型 | 无符号整型 | uint8 ~ uint256 |
8-256位(步长为8) | 0 |
uint8 small = 255; uint256 big = 10**18; |
| 地址型 | 地址 | address |
20字节 | 0x000... |
address owner = 0x...; |
| 地址型 | 可支付地址 | address payable |
20字节 | 0x000... |
address payable recipient; |
| 定长字节 | 字节数组 | bytes1 ~ bytes32 |
1-32字节 | 全0字节 | bytes32 hash = keccak256("data"); |
| 动态字节 | 字节数组 | bytes |
动态大小 | 空数组 | bytes memory data = new bytes(10); |
| 动态字节 | 字符串 | string |
动态UTF-8字符串 | 空字符串 | string name = "Alice"; |
| 数组 | 定长数组 | T[k] |
固定长度k | 每个元素为默认值 | uint[5] fixedArr; |
| 数组 | 动态数组 | T[] |
动态长度 | 空数组 | uint[] dynamicArr; |
| 结构体 | 结构体 | struct |
自定义类型 | 各成员默认值 | struct Person { string name; uint age; } |
| 枚举 | 枚举 | enum |
自定义离散值 | 第一个元素 | enum State { Created, Active, Inactive } |
| 映射 | 哈希映射 | mapping(K => V) |
键值对存储 | 空映射 | mapping(address => uint) balances; |
| 函数 | 函数类型 | function |
函数可见性+状态可变性 | - | function(uint) external returns (uint) |
| 特殊类型 | 合约类型 | contract |
合约实例 | - | MyContract instance; |
| 特殊类型 | 接口类型 | interface |
接口实例 | - | IERC20 token; |
| 特殊类型 | 库类型 | library |
库实例 | - | using SafeMath for uint; |
附加说明
1. 整型类型
-
int和uint是int256和uint256的别名 -
支持常规算术、比较和位运算
2. 地址类型特性
-
address.balance:地址的以太币余额 -
address.transfer():发送以太币 -
address.send():发送以太币(返回bool) -
address.call():低级调用
3. 数据位置(对于引用类型)
-
storage:永久存储在区块链上 -
memory:临时存储,函数调用期间存在 -
calldata:不可修改的临时数据(用于函数参数)
4. 类型转换
-
隐式转换:小类型转大类型(
uint8→uint16) -
显式转换:可能溢出需要检查
-
字面量转换:
uint8 x = uint8(300);// 溢出为44
5. 特殊值
-
msg.sender:当前调用者地址 -
msg.value:随交易发送的以太币数量 -
block.timestamp:当前区块时间戳 -
block.number:当前区块号
6. 全局变量
rust
// 常用全局变量
address payable owner = payable(msg.sender);
uint timestamp = block.timestamp;
uint difficulty = block.difficulty;
uint gasleft = gasleft();
这个表格涵盖了Solidity的主要数据类型,实际开发中这些类型经常组合使用,特别是结构体、映射和数组的组合非常常见。
现在,我们先来关注最常用的几种:
-
布尔值(bool):真或假
-
无符号整数(uint):无符号整数(正数)
-
整数(int):有符号整数(正数和负数)
-
地址(address):20 字节的值。您可以在 MetaMask 帐户中找到地址示例。
-
字节(bytes):底层原始字节数据
7.1.1 变量定义
变量只是值的占位符。这个值可以是上述列表中描述的某种数据类型。例如,我们可以创建一个名为 `hasFavoriteNumber` 的布尔变量,用来表示某人是否有喜欢的数字(恒定值 `true` 或 `false`)。
rust
bool hasFavoriteNumber = true; // The variable `hasFavoriteNumber` represents the value `true`
可以为 uint 和 int 类型指定所使用的位数(bits)。例如,uint256 指定该变量占用 256 位。uint 是 uint256 的简写。
🗒️注意:在指定数据类型的长度时,最好明确说明。
每行末尾的分号表示该语句已完成。
rust
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract SimpleStorage {
// Basic types
bool hasFavoriteNumber = true;
uint256 favoriteNumber = 88;
string favoriteNumberInText = "eighty-eight";
int256 favoriteInt = -88;
// address myAddress = 0xaB1B7206AA6840C795aB7A6AE8b15417b7E63a8d; // 这个地址数据会报错
address myAddress = 0xAB1b7206aa6840C795aB7A6AE8b15417B7E63A8D;
bytes32 favoriteBytes32 = "cat";
}
7.1.2 字节和字符串
字节是以十六进制表示的 字符集合 。
rust
bytes1 minBytes = "I am a fixed size byte array of 1 byte";
bytes32 maxBytes = "I am a fixed size byte array of 32 bytes";
bytes dynamicBytes = "I am a dynamic array, so you can manipulate my size";
字节可以按大小分配(最大可达 1000 字节bytes32)。但是,bytes 和 bytes32 代表不同的数据类型。字符串在内部以动态字节数组(类型)的形式表示bytes,专为处理文本而设计。因此,字符串可以很容易地转换为字节。++比特和字节概述++
7.2 合约逻辑
让我们来探讨这样一个场景:有一项任务涉及存储一个最喜欢的数字。为此,我们可以先存储一个类型为 `uint` 的变量 `favoriteNumber`:
👀❗重要提示: Solidity 中的每个变量都有一个默认值 。例如,未初始化的 uint256 类型变量默认值为零
0,未初始化的布尔类型变量默认值为false。
7.3 结论
你刚刚用变量填写了你的第一个智能合约,并且探索了 Solidity 中的基本数据类型。
7.4 测试一下自己:
问题1:变量和值有什么区别?
在Solidity中,变量 是存储数据的命名容器(如`uint favoriteNumber`),而值则是存储在该容器中的具体数据(如`42`)。简单来说:变量只是值的占位符。
问题2:请描述以下类型的默认值:bool、uint、int256、string、address、bytes、bytes32。
| 数据类型 | 关键字 | 默认值 | 说明 |
|---|---|---|---|
| 布尔型 | bool |
false |
布尔类型的零值 |
| 无符号整数 | uint |
0 |
等同于 uint256,所有位为0 |
| 有符号整数 | int256 |
0 |
所有位为0 |
| 字符串 | string |
"" |
空字符串 |
| 地址类型 | address |
0x0000000000000000000000000000000000000000 |
20字节的全零地址 |
| 动态字节数组 | bytes |
0x |
空字节数组 |
| 定长字节数组 | bytes32 |
0x0000000000000000000000000000000000000000000000000000000000000000 |
32字节的全零值 |
示例代码:
rust
// 声明变量但不初始化,将获得默认值
bool public b; // false
uint public u; // 0 (uint256)
int256 public i; // 0
string public s; // ""
address public a; // 0x0000000000000000000000000000000000000000
bytes public bs; // 0x
bytes32 public b32; // 0x0000000000000000000000000000000000000000000000000000000000000000
注意:
(1)所有类型在声明时如果没有显式初始化,都会被自动赋予默认值。
(2)在合约构造函数或函数中,未初始化的状态变量会自动获得默认值。
(3)默认值在Solidity中也称为"零值"(zero value)。
(4)对于引用类型(如数组、映射、结构体),其默认值也遵循类似的零值规则。
问题3:uint 与 bytes 有什么区别?
在Solidity中,uint(无符号整数)和bytes(动态字节数组)是两种完全不同的数据类型:uint用于存储非负整数值(如数字42),是固定大小的值类型,默认值为0,适用于数值计算和状态计数;而bytes用于存储任意长度的原始字节数据(如十六进制数据0x48656c6c6f),是动态大小的引用类型,默认值为空字节数组0x,常用于处理二进制数据、加密哈希或编码字符串。两者在内存表示、操作方式和应用场景上有本质区别,uint关注数值运算,bytes关注字节级数据操作。
问题4:编写一个智能合约,其中至少包含五个存储变量,每个变量具有不同的数据类型。
rust
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MultiDataStorage {
// 1. 布尔型:存储状态标志
bool public isActive;
// 2. 无符号整数:存储最喜欢的数字
uint256 public favoriteNumber;
// 3. 地址:存储合约所有者
address public owner;
// 4. 字符串:存储代币名称
string public tokenName;
// 5. 定长字节数组:存储数据哈希
bytes32 public dataHash;
// 6. 结构体:存储用户信息(演示复杂类型)
struct User {
address wallet;
uint256 balance;
bool isVerified;
}
// 7. 映射:存储地址到用户信息的映射
mapping(address => User) public users;
// 8. 动态数组:存储交易哈希列表
bytes32[] public transactionHashes;
// 构造函数,初始化合约时设置默认值
constructor() {
isActive = true;
favoriteNumber = 42;
owner = msg.sender;
tokenName = "MyToken";
dataHash = keccak256(abi.encodePacked("initial data"));
// 初始化一个默认用户
users[msg.sender] = User({
wallet: msg.sender,
balance: 1000,
isVerified: true
});
// 添加一个初始交易哈希
transactionHashes.push(dataHash);
}
// 设置最喜欢的数字
function setFavoriteNumber(uint256 _number) public {
require(isActive, "Contract is not active");
favoriteNumber = _number;
}
// 切换激活状态
function toggleActive() public {
require(msg.sender == owner, "Only owner can toggle");
isActive = !isActive;
}
// 更新数据哈希
function updateDataHash(string memory _data) public {
dataHash = keccak256(abi.encodePacked(_data));
}
// 添加新用户
function addUser(address _user, uint256 _balance, bool _verified) public {
users[_user] = User({
wallet: _user,
balance: _balance,
isVerified: _verified
});
}
// 添加交易哈希
function addTransactionHash(bytes32 _txHash) public {
transactionHashes.push(_txHash);
}
// 获取交易哈希数量
function getTransactionCount() public view returns (uint256) {
return transactionHashes.length;
}
// 获取合约总用户数(通过事件记录,实际需要跟踪)
// 注意:映射不能直接获取长度,这里只是示例
// 获取合约信息摘要
function getContractInfo() public view returns (
bool activeStatus,
uint256 currentFavoriteNumber,
address contractOwner,
string memory currentTokenName,
bytes32 currentDataHash,
uint256 userCountEstimate
) {
// 注意:实际项目中需要单独维护用户计数
activeStatus = isActive;
currentFavoriteNumber = favoriteNumber;
contractOwner = owner;
currentTokenName = tokenName;
currentDataHash = dataHash;
userCountEstimate = 0; // 简化示例,实际需要计数
}
}
第8节:函数
8.1 介绍
在上一课中,我们在第一个智能合约中添加了存储变量 favoriteNumber,并探索了不同的 Solidity 类型。在本课中,你将学习如何更新和检索一个存储变量,同时了解函数、可见性、部署、交易、gas 消耗以及变量作用域。
8.2 创建存储函数
📋 要存储 favoriteNumber 变量,我们需要实现一个新函数。在 Solidity 中,函数(或方法)是代码中为执行特定任务而设计的部分。我们将这个新函数命名为 store,它将负责更新 favoriteNumber 变量。
rust
contract SimpleStorage {
uint256 favoriteNumber; // a function will update this variable
// the function will be written here
}
函数通过关键字 function 来标识,后跟一个自定义名称(例如store)以及圆括号 () 内包含的参数。这些参数表示传递给函数的值。在本例中,我们告知store函数,希望通过另一个值 _favoriteNumber 来更新 favoriteNumber。
rust
contract SimpleStorage {
uint256 favoriteNumber; // storage variable: it's stored in a section of the blockchain called "Storage"
function store(uint256 _favoriteNumber) public {
// the variable favorite number is updated with the value that is contained into the parameter `_favoriteNumber`
favoriteNumber = _favoriteNumber;
}
}
函数的内容放在花括号 {} 内。在 _favoriteNumber 前加上前缀 _ 是为了强调这个局部变量 _favoriteNumber 与状态变量 favoriteNumber 是不同的变量。这有助于在复杂代码库中处理名称相似的变量时避免潜在的混淆。
8.3 部署智能合约
你可以在 Remix VM 环境中测试这个函数。

此时,你可以通过导航到<Solidity compiler>,并点击"Complie"来编译你的代码。编译完成后,切换到<Deploy & run transactions>选项卡来测试你的函数。

Deploy & run transactions 选项卡包含了许多在部署过程中使用的参数。你将获得一个分配有少量 ETH 的账户,用于部署你的智能合约

在此环境下,您的合约将被分配一个唯一地址。
rust
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
您可以通过展开<Depolyed Contracts>界面并同时打开终端来重新访问已部署的合约,终端将显示所有合约部署和交易的日志数据。

打开 Remix 终端后,我们可以看到合约部署已在 Remix 环境中发送了一笔模拟交易。您可以查看其详细信息,例如状态、哈希值、发起方、接收方和 gas 费用。
👀❗重要提示:部署合约和发送以太币的交易流程相同。唯一的区别在于,已部署合约的机器可读代码会被放置在部署交易的数据字段中。
8.4 创建交易
让我们向 store 函数发送一笔交易来更改变量 favoriteNumber 的值:你可以在 Remix 中输入一个数字并按下 store 按钮。一笔交易将被发起,一段时间后,其状态将从"待处理"变为"完成"。

⚠️ 从账户部分可以看出,每次提交交易时都会消耗 ETH。当区块链的状态被修改时(例如部署合约、发送 ETH......),都是通过发送消耗 gas(燃料)的交易来完成的。执行 store 函数比单纯在账户间转移 ETH 成本更高,而不断上涨的 gas 费用主要(尽管不完全是)与代码长度相关。
1、验证存储的值
该合约缺少检查数字是否已更新的方法:现在我们可以存储一个值,但我们无法确定交易是否真正更改了变量值。
该变量的默认可见性favoriteNumber为内部可见,防止外部合同和用户查看。
🗒️注意:在变量旁边附加
public关键字会自动更改其可见性,并会生成一个 getter 函数(即调用时获取变量值的函数)。
rust
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;
contract SimpleStorage {
uint256 public favoriteNumber; // storage variable: it's stored in a section of the blockchain called "Storage"
function store(uint256 _favoriteNumber) public {
// the variable favorite number is updated with the value that is contained into the parameter `_favoriteNumber`
favoriteNumber = _favoriteNumber;
}
}
**编译和部署完成后,**在<Deployed Contracts>可以看到两个部署的智能合约。后面一个是我们新部署的智能合约。

点开新部署的智能合约,并在store中输入相对应的数据,favoriteNumber会显示一个标有"返回"的按钮。按下该按钮,应返回变量的最新存储值favoriteNumber。

2、能见度
Solidity 中的函数与变量可通过以下四种可见性修饰符定义其可访问范围:
-
🌎 public(公开):可在合约内部及外部(包括其他合约与交易)访问。
-
🏠 private(私有):仅限当前合约内部访问。它并不隐藏数据(区块链数据本身公开),仅限制代码层面的直接访问。
-
🌲 external (外部):仅适用于函数。只能从合约外部调用,合约内部不可直接访问(除非通过 `this.func()` 形式进行外部调用)。
-
🏠 internal(内部):允许当前合约及其派生合约访问,外部无法调用。
此外,状态变量默认可见性为 internal ,函数默认可见性为 public(但建议显式声明以提升代码可读性与安全性)。
3、view和pure关键词
view 和 pure 这两个术语用于描述从区块链读取值而不改变其状态的函数。这类函数不会发起交易,而是进行调用,在 Remix 界面中以蓝色按钮表示。pure 函数则不允许从状态或存储中读取任何数据。
rust
// SPDX-License-Identifier: MIT
// 声明许可证标识符,通常使用MIT许可证,表示代码的开源许可
pragma solidity >=0.8.2 <0.9.0;
// 声明Solidity编译器版本范围,要求版本在0.8.2及以上,但低于0.9.0
contract SimpleStorage {
// 定义一个名为SimpleStorage的智能合约
// 存储变量:该变量会被永久存储在区块链的"Storage"区域
// public关键字会自动生成一个getter函数,允许外部读取该变量的值
uint256 public favoriteNumber;
// store函数:用于更新favoriteNumber变量的值
// 参数_favoriteNumber:要设置的新值(局部变量,前缀_用于区分状态变量)
// public可见性:允许从合约内部和外部调用此函数
// 注意:调用此函数会修改区块链状态,因此需要消耗gas并产生交易
function store(uint256 _favoriteNumber) public {
// 将状态变量favoriteNumber更新为参数_favoriteNumber的值
favoriteNumber = _favoriteNumber;
}
// retrieve函数:用于读取favoriteNumber变量的当前值
// view修饰符:表示此函数只读取区块链状态而不修改它
// public可见性:允许从合约内部和外部调用此函数
// 注意:调用此函数不会修改区块链状态,因此不消耗gas(仅本地调用)
function retrieve() public view returns (uint256) {
// 返回favoriteNumber变量的当前值
return favoriteNumber;
}
}

rust
// SPDX-License-Identifier: MIT
// 声明许可证标识符,通常使用MIT许可证,表示代码的开源许可
pragma solidity >=0.8.2 <0.9.0;
// 声明Solidity编译器版本范围,要求版本在0.8.2及以上,但低于0.9.0
contract SimpleStorage {
// 定义一个名为SimpleStorage的智能合约
// 存储变量:该变量会被永久存储在区块链的"Storage"区域
// public关键字会自动生成一个getter函数,允许外部读取该变量的值
uint256 public favoriteNumber;
// store函数:用于更新favoriteNumber变量的值
// 参数_favoriteNumber:要设置的新值(局部变量,前缀_用于区分状态变量)
// public可见性:允许从合约内部和外部调用此函数
// 注意:调用此函数会修改区块链状态,因此需要消耗gas并产生交易
function store(uint256 _favoriteNumber) public {
// 将状态变量favoriteNumber更新为参数_favoriteNumber的值
favoriteNumber = _favoriteNumber;
}
// retrieve函数:用于读取favoriteNumber变量的当前值
// view修饰符:表示此函数只读取区块链状态而不修改它
// public可见性:允许从合约内部和外部调用此函数
// 注意:调用此函数不会修改区块链状态,因此不消耗gas(仅本地调用)
function retrieve() public view returns (uint256) {
// 返回favoriteNumber变量的当前值
return favoriteNumber;
}
// retrieve1函数:一个纯函数示例,用于演示pure函数特性
// pure修饰符:表示此函数既不读取也不修改区块链状态
// 注意:该函数不能访问任何状态变量(如favoriteNumber),只能使用局部变量或直接返回值
// public可见性:允许从合约内部和外部调用
// returns(uint256):声明函数返回一个uint256类型的值
function retrieve1() public pure returns(uint256) {
// 由于是pure函数,以下代码如果取消注释会编译错误:
// return favoriteNumber; // 错误:pure函数不能读取状态变量
// pure函数只能返回固定值或基于参数计算的值(这里返回固定值7)
// 调用此函数不会消耗gas(本地调用),也不会产生区块链交易
return 7; // 返回一个固定的uint256值7
}
}

该关键字returns指定函数将返回的值的类型。
🚧警告:虽然调用
view函数pure本身通常不需要 gas,但如果被另一个通过事务修改状态或存储的函数调用(例如,retrieve在函数内部调用该函数storage),则需要 gas。这部分成本称为执行成本,它会累加到事务成本中。
8.5 变量的作用域
变量的作用域指的是变量被定义和访问的上下文。这个上下文通常由声明变量的代码块(一般由花括号 { } 包裹)决定。要在不同的函数中访问同一个变量,应该将该变量声明在主合约的作用域内。
rust
// SPDX-License-Identifier: MIT
// 开源许可证标识,使用MIT许可证
pragma solidity >=0.8.2 <0.9.0;
// Solidity编译器版本声明:版本需在0.8.2到0.9.0之间(含0.8.2,不含0.9.0)
// 定义一个名为SimpleStorage的智能合约
contract SimpleStorage {
// 状态变量:存储在区块链存储区中的变量,在整个合约范围内可见
// public关键字会自动生成一个getter函数,允许外部读取该变量值
uint256 public favoriteNumber;
// store函数:用于更新favoriteNumber变量的值
// 参数_favoriteNumber:要设置的新值
// public可见性:允许从合约内部和外部调用
function store(uint256 _favoriteNumber) public {
// 将状态变量favoriteNumber更新为参数传递的值
favoriteNumber = _favoriteNumber;
// 局部变量:仅在store函数内部有效
// 声明一个名为testVar的局部变量并赋值为5
// 该变量在函数执行结束后会被销毁,不会存储在区块链上
uint256 testVar = 5;
// 注意:由于testVar是局部变量,外部无法访问,且函数结束后不复存在
}
// something函数:演示变量作用域的函数
// public可见性:允许从合约内部和外部调用
function something() public {
// 这行代码将导致编译错误:
testVar = 6; // 错误:testVar未定义
// 原因:testVar是在store函数中定义的局部变量,只在store函数内部可见
// 在something函数中无法访问另一个函数中定义的局部变量
// 这行代码是合法的:
favoriteNumber = 7; // 正确 可以访问,因为favoriteNumber是在主合约作用域中定义的
// 原因:favoriteNumber是在合约级别定义的存储变量,在整个合约的所有函数中都可见
// 所有合约内的函数都可以访问和修改这个状态变量
}
}
8.6 结论
在本课中,你学习了如何在 Solidity 中构建函数、定义其可见性,以及了解它如何在智能合约中操作值。你还探索了不同的交易及其 gas 消耗。
8.7 测试一下自己
问题1: 描述四个可见性关键词及其对代码的影响?
**(1)public:**可在合约内部和外部访问;
**(2)private:**仅限当前合约内部访问。它并不隐藏数据(区块链数据本身公开),仅限制代码层面的直接访问。
(3)external: 仅适用于函数。只能从合约外部调用,合约内部不可直接访问(除非通过this.func()形式进行外部调用)。 **(4)internal:**允许当前合约及其派生合约访问,外部无法调用。
问题2: view 和 pure 有什么区别?
在 Solidity 中,view 函数可以读取但不能修改合约状态变量,而 pure 函数既不能读取也不能修改状态变量,仅能使用传入参数和局部变量进行纯计算;两者在被外部调用时都不消耗 gas,但内部调用时可能产生 gas 成本。
问题3:在哪些情况下, pure 函数会产生燃气费用?
当 pure 函数在以下情况被调用时会产生燃气费用:
-
被非 view/pure 函数内部调用时,因为外部调用者发起的交易本身就需要支付 gas;
-
在链上交易中被直接调用(而非通过静态调用),即使不修改状态,执行计算仍需消耗 gas;
-
被其他合约通过外部调用时,Gas 费用由调用方承担。只有在通过
static call进行只读调用且未被嵌套在消耗性操作中时,pure 函数才可能免 Gas。
问题4:解释什么是 作用域 ,并举例说明一个不正确的作用域。
**作用域(Scope)是指程序中定义变量的有效区域,在此区域外该变量无法被访问。**一个典型的不正确作用域是在代码块外部访问其内部定义的局部变量。例如在Solidity中:
function incorrectScope() public pure {
if (true) {
uint blockScoped = 10; // 变量在 if 块内定义
}
uint x = blockScoped; // 错误!此处无法访问块内的变量
}
这里 blockScoped 的作用域仅限于 if 代码块内部,在块外部访问它会导致编译错误。正确的做法是将变量声明在需要访问的同一作用域层级。
问题5:部署合约的交易和转移 ETH 的交易有什么区别?
部署合约的交易和转移 ETH 的交易在区块链上的核心区别在于:部署合约的交易 的接收方地址为空,其 data 字段包含合约的字节码和构造函数参数,执行后会在链上创建一个新的合约账户;而转移 ETH 的交易 接收方是一个明确的账户地址,data 字段通常为空或仅包含简单备注,执行后只改变双方 ETH 余额,不创建新合约。两种交易都消耗 Gas,但部署合约通常成本更高,因为需要存储字节码和初始化状态。
问题6:编写一份包含以下三个功能的智能合约:
(1)一个只能由当前合约访问的view函数。
(2)一个在当前合约内部不可访问的pure函数。
(3)一个可以从子合约访问的view函数。
rust
pragma solidity ^0.8.0;
contract MyContract {
// (1) 只能由当前合约访问的view函数 - 使用private可见性
function privateViewFunc() private view returns(uint) {
return 10;
}
// 示例:在合约内部调用private函数
function usePrivateView() public view returns(uint) {
return privateViewFunc();
}
// (2) 在当前合约内部不可访问的pure函数 - 使用external可见性(外部无法内部调用)
function externalPureFunc() external pure returns(uint) {
return 20;
}
// 注意:如果在合约内部调用externalPureFunc()会编译错误,必须通过this.externalPureFunc()
// (3) 可以从子合约访问的view函数 - 使用internal可见性
function internalViewFunc() internal view returns(uint) {
return 30;
}
}
contract ChildContract is MyContract {
// 子合约可以访问父合约的internal函数
function getInternalData() public view returns(uint) {
return internalViewFunc(); // 正确:可以访问internal函数
// 不能访问privateViewFunc() - 编译错误
}
}
第09节:数组和结构体
9.1 介绍
截至目前,SimpleStorage合约仅支持存储、更新和查看单个喜欢的数字。本节课程中,我们将改进代码以实现存储多个数字的功能,使多人能够分别保存自己的数值。我们将学习如何使用数组创建喜欢的数字列表,并探索如何在Solidity中使用struct关键字创建新类型。
9.2 数组和结构体
首先,我们需要将 uint256 favoriteNumber 替换为一个 uint256 类型的数字列表:
rust
uint256[] list_of_favorite_numbers;
方括号表示我们有一个 uint256 类型的列表,即一个数字数组。如果我们想初始化这个数组,可以通过指定其内容来实现:
rust
uint256 [] list_of_favorite_numbers = [ 0 , 78 , 90 ];
🗒️注意:数组的索引从零开始:第一个元素位于位置 0(索引为 0),第二个元素位于位置 1(索引为 1),依此类推。
这种数据结构的问题在于我们无法将所有者与其偏好的数值关联起来。一种解决方案是使用 struct 关键字定义一个新类型,命名为 Person,它由两个属性构成:一个偏好数字和一个姓名。
rust
struct Person {
uint256 my_favorite_number;
string name;
}
🚧警告:请重命名变量
favorite_number以避免名称冲突。
基于这个结构体,我们可以实例化 一个类型为 Person 的变量 my_friend,其偏好数字为7,姓名为'Pat'。我们可以通过 public 关键字自动生成的获取函数来检索这些详细信息。
rust
Person public my_friend = Person(7, 'Pat');
/* equals to
Person public my_friend = Person({
favorite_number:7,
name:'Pat'});
*/
9.2 结构体数组
为每个人创建单独的变量可能会因重复的步骤而变成一项繁琐的任务。我们可以结合刚刚学到的两个概念------数组与结构体,来避免手动为每个人实例化变量。
rust
Person[] public list_of_people; // this is a dynamic array
Person[3] public another_list_of_three_people; // this is a static array
使用动态数组时,我们可以根据需要添加任意数量的Person对象,因为数组的大小不是固定的,而是可以增长和缩小的。我们可以Person通过索引访问数组中的每个对象。
要向此列表中添加人员,我们可以创建一个函数来填充数组:
rust
// 添加人员信息的公共函数
function add_person(string memory _name, uint256 _favorite_number) public {
// 使用传入的姓名和偏好数字创建一个新的 Person 结构体实例
// 并通过 push 方法将其添加到 list_of_people 数组中
list_of_people.push(Person(_favorite_number, _name));
}
add_person这是一个函数,它接受两个变量作为输入------人的姓名和最喜欢的电话号码。它首先创建一个新Person对象,然后将其添加到我们的list_of_people数组中。
9.3 结论
借助这些特性,我们的 Solidity 合约现在可以存储多个喜爱的号码,每个号码都与特定的人关联。该add_person函数会创建一个新的Person结构体并将其添加到状态变量中。然后,我们可以通过数组索引list_of_people访问该对象,查看每个人的姓名和喜爱的号码。Person
9.4 测试一下自己
(1)请定义动态数组和静态数组的区别,并分别举例说明。
-
长度是否固定:静态数组在声明时指定固定长度,无法改变;动态数组长度可变,可在运行时增减元素。
-
内存分配:静态数组在编译时确定内存大小;动态数组在运行时动态分配内存。
-
使用场景:静态数组适用于元素数量已知且固定的场景;动态数组适用于需要灵活增减元素的场景。
rust
// 静态数组:长度固定的数组
uint256[3] public staticArray; // 固定只能存储3个元素
// 动态数组:长度可变的数组
uint256[] public dynamicArray; // 长度不固定,可动态调整
// 静态数组初始化
function initStaticArray() public {
staticArray = [1, 2, 3]; // 必须正好3个元素
// staticArray.push(4); // 错误!静态数组不能使用push
// staticArray.length = 5; // 错误!不能改变长度
}
// 动态数组使用
function useDynamicArray() public {
dynamicArray.push(1); // ✅ 长度变为1
dynamicArray.push(2); // ✅ 长度变为2
dynamicArray.pop(); // ✅ 删除最后一个元素,长度变为1
dynamicArray.push(3); // ✅ 长度再次变为2
// 可以随时添加更多元素
for(uint i = 0; i < 5; i++) {
dynamicArray.push(i * 10);
}
// 现在长度为7
}
(2)什么是数组?什么是结构体?
在 Solidity 中,数组(Array)是一种用于存储相同类型 元素的有序集合,可以通过索引访问每个元素,分为长度固定的静态数组和长度可变的动态数组;而结构体(Struct)是一种用户自定义的复合数据类型 ,允许将多个不同类型 的变量组合成一个单一的逻辑单元,用于描述具有多个属性的复杂实体。例如,可以用一个 Person[] 数组来存储多个 Person 结构体,其中每个结构体包含 name(字符串类型)和 age(整数类型)等不同属性。
(3)创建一个智能合约,用于存储和查看动物列表。手动添加三种动物,并允许用户手动向智能合约中添加无限数量的动物。
rust
// SPDX-License-Identifier: MIT
// 声明开源许可证类型(MIT许可证)
pragma solidity ^0.8.19;
// 指定Solidiaty编译器版本为0.8.19或更高(但低于0.9.0)
contract SimpleStorage {
// 定义一个名为SimpleStorage的智能合约
// 定义Animal结构体,用于表示动物信息
struct Animal{
string name; // 动物的名称,字符串类型
uint256 number; // 动物的编号,无符号256位整数类型
}
// 声明一个公有的动态数组,用于存储多个Animal结构体实例
Animal[] public listofanimal;
// 添加动物的公共函数
function add_animal(string memory _name, uint256 _number) public {
// 根据传入的名称和编号创建一个新的Animal结构体实例
// 并将其添加到listofanimal数组的末尾
listofanimal.push(Animal(_name, _number));
}
}

第10节:错误和警告
10.1 介绍
在之前的课程中,我们学习了如何结合使用数组 和结构体 来存储信息,以及如何通过 addPerson 函数操作这些信息。这次我们将探讨错误 与警告的处理方式,以及如何有效利用论坛、搜索引擎和 AI 资源来解决问题。
10.2 错误和警告
如果我们从代码中移除一个分号,然后尝试编译它,你会遇到一些🚫错误信息。这些错误信息会阻止编译器将代码转换为机器可读的形式。

将分号恢复到正确位置可以避免任何错误,使我们能够继续将代码部署到 Remix 虚拟机。
另一方面,如果我们从代码顶部删除 SPDX 许可证标识符并重新编译,我们将收到一个显示⚠️警告的黄色框。
警告:源文件中未提供 SPDX 许可证标识符

与错误不同,警告允许代码编译和部署,但最好认真对待警告并力求将其完全消除。警告会指出代码中不良或有风险的做法,有时还会提示潜在的错误。
-
如果显示红色,则表示代码中存在编译错误,需要在部署前解决。
-
如果是黄色,您可能需要仔细检查并调整您的代码。
10.3 充分利用你的资源
当您无法理解提示的错误信息时,使用一些在线资源可以帮助您更好地理解:
-
AI伙伴(ChatGPT、Phind、Bard、AI浏览器扩展等)
-
GitHub讨论区
-
Stack Exchange以太坊社区
-
Peeranha
10.4 Phind
现在让我们尝试使用 Phind 来解决之前故意制造的分号错误。Phind 是一个面向开发者的 AI 驱动搜索引擎。它的工作原理是首先基于你的查询进行谷歌搜索,然后解析搜索结果,为你提供上下文相关的答复。
我们可以在下拉菜单中输入编译器错误信息,执行搜索,并获得关于错误原因及如何修复的全面说明。

10.5 其他资源
建议积极使用人工智能工具,因为它们可以显著提升您的理解力和技能。在本课程的后续部分,我们将探讨如何提出有效的问题、利用人工智能提示、构建问题结构以及改进搜索和学习技巧。
您还可以参与GitHub 讨论和Stack Exchange等在线社区,在那里您可以找到宝贵的见解、问题的答案以及来自其他开发者的支持。
💡提示: 成为一名优秀的软件工程师或高效的工程师,最重要的方面之一不仅是拥有信息,还要知道在哪里可以找到信息。
10.6 结论
您刚刚学习了如何有效地识别和管理错误和警告,从而提升了维护健壮可靠代码的能力(代码的健壮性)。在接下来的课程中,我们将深入探讨 Solidity 的数据位置以及一些高级的 Remix 功能。
10.7 测试一下自己
(1)警告和错误有什么区别?请分别举例说明。
Solidity中的**警告(Warning)是编译器对潜在问题或非致命性代码问题的提示,不会阻止编译和部署(例如变量未使用会提示 Warning: Unused local variable );而 错误(Error)**是编译器遇到的致命问题,必须修复才能继续编译(例如缺少分号会报Error: Expected ';' but got '}'并中断编译)。
(2)制作一份包含至少 3 个有用在线资源的书面清单(或在浏览器中添加书签)将有助于您解决未来的错误。
第11节:内存存储和调用数据
11.1 介绍
在本节中,我们将探讨 Solidity 如何管理数据存储,重点介绍存储、内存和调用数据之间的区别,以及为什么这些概念对于编写优化和安全的智能合约至关重要。
11.2 数据位置
Solidity 可以将数据存储在六个不同的位置。本课将重点介绍前三个位置:
(1)调用数据 (Calldata) :只读 的临时数据区域,专门用于存储函数调用时传入的外部参数,其内容在交易执行期间不可修改。
(2) 内存 (Memory) :可读写 的临时存储空间,用于函数执行期间存放局部变量,函数调用结束后数据即被清除。
(3)存储 (Storage):持久化的状态存储区域,用于保存合约中声明的状态变量,数据会永久记录在区块链上且读写成本较高。
(4)栈 (Stack):用于保存小型局部变量和控制函数调用上下文的内存结构,其容量有限且仅限内部操作使用。
(5)代码 (Code):只读的存储区域,存放当前合约的编译后字节码,可供其他合约通过调用访问但无法修改。
(6)日志 (Logs):用于记录合约事件和状态变更的特殊存储结构,其数据无法被合约直接读取,但可供外部应用高效检索。
11.3 调用数据和内存
11.3.1 calldata 和 memory介绍
在Solidity中,calldata 和 memory 是函数执行期间变量的临时存储位置。calldata 是只读的,用于存储不可修改的函数输入参数。相比之下,memory 允许读写访问,变量可以在函数内部被修改。若要修改 calldata 中的变量,必须先将它们加载到 memory 中。
🚧警告:大多数变量类型会自动默认为 memory。然而,对于字符串,由于数组在内存中的处理方式,你必须明确指定使用 memory 或 calldata。
rust
string memory variableName = "someValue";
11.3.1 调用数据(calldata)
调用数据变量是只读的 ,而且比内存更便宜。它们主要用于存储输入参数。
在以下示例中,如果我们尝试将关键字替换memory为calldata,则会收到错误,因为calldata不能操作变量。
rust
function addPerson(string calldata _name, uint256 _favoriteNumber) public {
// 错误:calldata 参数是只读的,不能修改
// 试图修改只读的 calldata 参数会导致编译错误
_name = "cat"; // ❌ 这一行应该删除或修改为使用新的内存变量
// 使用传入的参数创建 Person 结构体并添加到数组
// 注意:由于上面的错误,此处的 _name 已被错误地修改尝试
// 实际上如果修复错误,这里应该使用原始传入的 _name
listOfPeople.push(Person(_favoriteNumber, _name));
}

11.4 存储storage
存储storage在区块链上的变量是持久的 ,在函数调用和交易之间保持其值。在我们的合约中,该变量myFavoriteNumber是存储变量。在任何函数外部声明的变量都会被隐式转换为存储变量。
rust
contract MyContract {
uint256 favoriteNumber; // this is a storage variable
};
11.5 字符串和基本类型
如果尝试为 uint256 类型的变量指定 memory 关键字,就会遇到这个错误:
bash
> Data location can only be specified for array, struct, or mapping type
注意:这个错误是因为在Solidity中,只有array 、struct 和mapping 类型需要显式指定数据位置(如
memory、calldata或storage),而基本类型(如uint256、bool、address等)不能指定数据位置,因为它们默认存储在栈上。

在Solidity中,string(字符串)被视作字节数组;而原始类型(如uint256)则拥有内置机制,明确规定其存储位置、访问方式及操作方法。
🚧警告:在 Solidity 中,你不能在函数内部使用
storage关键字。这里只允许使用memory和calldata,因为变量只是临时存在的。
bash
function addPerson(string memory _name, uint256 _favoriteNumber) public { // cannot use storage as input parameters
uint256 test = 0; // variable here can be stored in memory or stack
listOfPeople.push(Person(_favoriteNumber, _name));
}
11.6 结论
做得好!你已经了解了 Solidity 中关键字 storage、memory 和 calldata 之间的区别,提高了你开发强大的基于以太坊的应用程序的技能。
11.7 测试一下自己
Solidity 编译器在内存管理方面是如何处理基本类型和字符串的?
(1)为什么函数内部的变量不能使用 storage 关键字?
在Solidity中,函数内部的变量不能使用storage关键字声明,因为storage特指合约的持久化状态存储区域(数据永久保存在区块链上),而函数内部的局部变量本质是临时变量,其生命周期仅限于函数执行期间,若允许声明新的storage变量会混淆持久化存储与临时存储的界限,可能导致意外修改链上状态或产生高昂的Gas消耗;但可以通过storage指针引用已存在的状态变量,而非创建新的存储空间。
(2)编写一个智能合约,使用 storage、memory 和 calldata 关键字作为其变量。
bash
// SPDX-License-Identifier: MIT
// 开源许可证声明,使用MIT许可证允许自由使用代码
pragma solidity ^0.8.19;
// 指定Solidity编译器版本,^表示0.8.19及以上但低于0.9.0的版本
contract StorageExample {
// storage变量 - 状态变量默认存储在区块链的永久存储中
uint256 public count; // 基本类型状态变量,自动存储在storage中
string[] public items; // 动态数组状态变量,存储在storage中
// 定义Person结构体类型
struct Person {
string name; // 字符串类型,在storage中需要更多空间
uint256 age; // 基本类型
}
Person[] public people; // 结构体动态数组,存储在storage中
// 使用calldata参数的函数 - 最节省Gas的参数传递方式
function addItem(string calldata _item) external {
// _item是calldata参数:只读,来自交易调用数据,不能修改
// 将calldata复制到memory以便修改
string memory tempItem = _item; // memory变量:临时可修改存储
// 修改memory变量(calldata变量不能修改)
tempItem = string(abi.encodePacked(tempItem, "_added"));
// 将memory变量永久保存到storage状态变量
items.push(tempItem); // 修改storage,消耗较多Gas
count++; // 修改storage状态变量
}
// storage指针使用示例 - 引用已存在的状态变量
function updateFirstPerson(string calldata _newName) external {
require(people.length > 0, "No people in array"); // 前置条件检查
// storage指针:引用people数组第一个元素的存储位置
Person storage firstPerson = people[0];
// 通过storage指针直接修改区块链状态
firstPerson.name = _newName; // 修改storage,永久性改变
}
// memory结构体操作示例
function createPerson(string calldata _name, uint256 _age) external returns (Person memory) {
// 在memory中创建临时结构体实例
Person memory newPerson = Person({
name: _name,
age: _age
});
// 将memory结构体复制到storage(永久保存)
people.push(newPerson); // 复制操作,消耗Gas
// 返回memory结构体(会复制到调用者)
return newPerson;
}
// 使用storage指针遍历数组
function increaseAllAges() external {
for (uint256 i = 0; i < people.length; i++) {
// 获取每个元素的storage指针
Person storage person = people[i]; // 引用现有存储位置
person.age += 1; // 直接修改区块链状态
}
}
// 比较calldata和memory参数的函数
function compareLocations(string calldata _calldataStr, string memory _memoryStr)
external
pure // pure函数:不读取也不修改状态
returns (bytes memory)
{
// _calldataStr是只读的(来自交易调用数据)
// _memoryStr是可修改的(调用者提供的memory参数)
// 修改memory字符串
string memory modified = string(abi.encodePacked(_memoryStr, "_modified"));
// 返回连接后的字节数组
return abi.encodePacked(_calldataStr, modified);
}
// 演示不同数据位置变量声明的函数
function demonstrateLocations() external view {
// 基本类型不能指定数据位置,默认在栈上
uint256 storageCount = count; // 从storage复制值到栈变量
uint256 memoryCount = count; // 同上,memory关键字对基本类型无效
// 以下代码会导致编译错误(基本类型不能指定数据位置):
// uint256 memory invalid;
// 引用类型在函数内部必须指定数据位置
Person[] memory memoryPeople = new Person[](0); // memory数组:临时存储
Person memory tempPerson = Person("temp", 20); // memory结构体:临时存储
// 注意:memory变量在函数结束后被销毁
// storage指针只能引用已存在的状态变量
}
}
第12节:映射
12.1 介绍
我们刚刚创建了一个合约,它将多个Person联系人的姓名和喜欢的数字存储在一个列表中。在本节中,您将学习映射的概念、功能以及何时使用映射更有优势。
12.2 避免代价高昂的迭代
如果我们只想知道某个人最喜欢的数字(例如Chelsea's),但我们的合约中保存的是一个(很长的)数组Person,那么我们需要遍历整个列表才能找到所需的值:
bash
list_of_people.add(Person("Pat", 7));
list_of_people.add(Person("John", 8));
list_of_people.add(Person("Mariah", 10));
list_of_people.add(Person("Chelsea", 232));
// Go through all the people to check their favorite number.
// If name is "Chelsea" -> return 232
遍历长长的数据列表通常成本高昂且耗时,尤其是不需要按索引访问元素时更是如此。
12.3 Mapping(映射)
为了直接访问目标值而无需遍历整个数组,我们可以使用映射(mappings) 。它们是一组(唯一的)键与值相关联的集合,类似于其他编程语言中的哈希表或字典。在我们的例子中,通过查找姓名(键)将返回其对应的偏好数字(值)。
映射的定义使用 mapping 关键字,后跟键的类型、值的类型、可见性以及映射的名称。在我们的示例中,我们可以构建一个将每个姓名映射到其偏好数字的对象。
bash
mapping (string => uint256) public nameToFavoriteNumber;
先前我们创建了一个 addPerson 函数,它将结构体 Person 添加到数组 List_of_people 中。现在让我们修改这个函数,将结构体 Person 添加到映射(mapping)中,而不是数组中:
bash
nameToFavoriteNumber[_name] = _favoriteNumber;
完整代码:
bash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract SimpleStorage{
uint256 public myFavoriteNumber;
// uint256[] listOfFavoriteNumbers
struct Person{
uint256 favoriteNumber;
string name;
}
// dynamic array
Person[] public listOfPeople;
mapping(string => uint256) public nameToFavoriteNumber;
function store(uint256 _favoriteNumber) public {
myFavoriteNumber = _favoriteNumber;
}
// view ,pure
function retrivev() public view returns(uint256){
return myFavoriteNumber;
}
// calldata , memory , storage
// String is a byte array;
function addPerson(string memory _name, uint256 _favoriteNumber) public{
listOfPeople.push(Person(_favoriteNumber,_name));
nameToFavoriteNumber[_name] = _favoriteNumber; // 将name 和 favoriteNumber 映射起来。
}
}

👀❗重要提示:映射的查找时间复杂度为常数,这意味着通过键检索值的时间复杂度为常数。
🗒️注意:所有键类型的默认值均为零。在本例中,
nameToFavoriteNumber["ET"]等于 0。
12.4 结论
在尝试从更大的数据集中查找元素时,映射可以成为提高效率的多功能工具。
12.5 测试一下自己
(1)在哪些情况下使用数组比使用映射更好?
在 Solidity 中,使用数组比映射更适合以下场景:
1)需要保持元素顺序时(如时间线、队列、堆栈),因为数组天然具有顺序性而映射是无序的;
2)需要遍历所有元素时(如计算总和、筛选符合条件的元素),数组可直接循环而映射需要额外维护键列表;
3)元素数量较少且需要频繁按索引访问 时,数组的索引访问(如arr[0])比映射的键值查找(如map[key])更直观且成本相当;
4)需要与其他系统交互时(如返回完整数据集),数组更符合传统编程习惯,便于前端或其他合约处理;
5)存储结构简单的值类型集合 时(如uint256[]),数组比映射(如mapping(uint256 => uint256))更节省存储空间。
例如,存储投票列表(需按时间顺序)或商品目录(需全部展示)时,数组是更优选择。
(2)创建一个包含名为 addressToBalance 映射的 Solidity 合约。 实现向该映射添加数据以及从该映射检索数据的函数。
bash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract SimpleStorage{
struct AddreBalance{
string addre;
uint256 balance;
}
// dynamic array
AddreBalance[] public addreBalance;
mapping(string => uint256) public addressToBalance;
function addPerson(string memory _addre, uint256 _balance) public{
addreBalance.push(AddreBalance(_addre,_balance));
addressToBalance[_addre] = _balance;
}
}

第13节:部署你的第一个合同
13.1 介绍
在之前的课程中,我们编写了SimpleStorage合约。它定义了一个自定义类型Person,包含一个可读取和更新的内部变量,以及一个可修改的公共数组和映射。在本课中,我们将把合约部署到真实的测试网上,该测试网完全模拟了真实的区块链环境,而无需使用真正的以太币。
🔥注意:您可能会想立即将此合约部署到主网上。但通常情况下,我不建议这样做。在将合约部署到生产环境之前,请务必编写测试、进行审计并确保其稳健性。不过,为了演示的目的,我们将把它作为虚拟合约部署到测试网上。
部署前,务必进行编译检查。这可以确保合约没有错误或警告,适合部署。
13.2 在测试网上部署
我们可以通过进入部署选项卡,并将环境从本地虚拟环境(Remix VM)切换到注入提供程序(MetaMask)来启动部署流程。此操作将允许 Remix 发送请求并与您的 MetaMask 帐户进行交互。

接下来,系统会提示您从 MetaMask 钱包中选择一个账户。将该账户连接到 Remix 后,您应该会看到账户已成功连接的确认信息,并且您正在使用 Sepolia 测试网。

请确保您的账户中有足够的 Sepolia ETH,您可以通过++水龙头++获取。余额充足后,即可点击"部署"按钮继续。之后,MetaMask 会请求对测试网上的交易进行签名并发送。

交易执行完毕后,合约地址将与交易详情一起列在已部署合约列表中。这就是部署交易在 Etherscan 上的显示方式。

13.3 合同互动
合约部署完成后,我们现在可以与其交互并更新区块链。例如,如果您想存储一个数字,可以点击"存储"按钮:MetaMask 会请求另一笔交易确认,确认后即可更新您收藏的数字。您可以在 etherscan 上查看已部署地址的详细信息:

👀❗重要提示:查看和纯函数不会发送交易
💡小贴士:庆祝小小的胜利和里程碑。这些心理上的鼓励会让你在学习过程中保持积极性。
只需切换 MetaMask 网络,即可将合约部署到不同的测试网或主网上。请确保您拥有足够数量的与网络兼容的 ETH 来部署合约。
13.4 结论
将 Solidity 合约部署到测试网是开发过程中至关重要的一步,它允许您在真实的区块链环境中测试合约的功能,而无需承担使用真实以太币的风险。请务必在部署前执行必要的审计和测试,以确认合约的安全性和正确性。
13.5 测试一下自己
(1)在将合约部署到测试网之前,应该采取哪些步骤?
在将合约部署到生产环境之前,请务必编写测试、进行审计并确保其稳健性。
(2)在 Sepolia 测试网上部署一个简单的 Solidity 合约。在etherscan上可以看到哪些重要信息?
第14节:小测试









第15节:ZKsync部署
15.1 介绍
随着部署到以太坊主网的成本不断攀升,诸如 Rollup 和 Layer 2 网络之类的扩容方案的需求日益增长。以下课程将指导您如何将智能合约部署到 L2 zkSync。如果您已学习过"区块链基础"部分的 zkSync 课程,则应该已经将 ZkSync 添加到您的 MetaMask 中。如果没有,我们将在下一课中讲解该过程。
15.2 测试网资金
要在 zkSync 上部署合约,您需要测试网资金。有两种方法可以获取测试网资金:
(1)zkSync 水龙头:类似于使用 Sepolia 水龙头,此方法允许您直接请求测试网资金。
(2)zkSync Bridge:此方法涉及将资金从以太坊测试网转移到 zkSync 测试网。虽然免费水龙头有时不太可靠,但桥接提供了一种更稳定的解决方案。
15.3 部署
要将合约部署到 ZKSync,需要遵循以下几个关键步骤。首先,必须在 MetaMask 钱包中配置 ZKSync。接下来,您需要获取测试网 ETH,这是部署和测试合约所必需的。
第16节:ZKsync桥接
16.1 介绍
在本课程中,我们将一步一步地指导您使用桥接 方法 在测试网上获取 zkSync ETH 。本课程中提到的所有链接都可以在与本课程关联的++GitHub 代码库++中找到。
16.2 钱包连接
首先,打开++zkSync Bridge++ 应用,点击"连接钱包"按钮。选择 MetaMask,并根据提示输入密码。连接成功后,请确保您已连接到Sepolia 测试网络。如果您缺少 Sepolia ETH,可以使用++GCP 水龙头++ 或其他++推荐的测试网水龙头++。
16.3 Bridging Sepolia
要将 Sepolia ETH 桥接到 zkSync,请选择 zkSync 桥接页面右上角的"zkSync"按钮,然后切换到++"zkSync Sepolia 测试网"++。界面将显示从以太坊 Sepolia 测试网桥接的选项。
16.4 资金转移
接下来,返回 MetaMask,向 zkSync Sepolia 转账少量 Sepolia ETH(即使是 0.001 ETH 也足以部署一个智能合约)。
👀❗重要提示:请务必使用测试网钱包,其中不包含任何真实资金。
选择"继续",然后进行资金转账。在 MetaMask 上确认交易,您的资金将在 15 分钟内到账。
等待期间,您可以将 zkSync Sepolia 测试网添加到 MetaMask。前往++Chainlist++,搜索"zkSync Sepolia"(搜索时请包含测试网),然后连接您的钱包。

批准添加网络后,即可切换到 zkSync Sepolia 测试网。

交易完成后,您将在 MetaMask 钱包的 zkSync Sepolia 测试网下看到资金到账。钱包资金到账后,您即可在 Remix 中部署合约。
第17节:ZKsync插件
17.1 介绍
在本课中,您将学习专业开发人员使用的同类型的二层部署或汇总部署。在 Remix 中,我们可以先在环境中激活zkSync 插件。在插件管理器中,搜索"zkSync"并激活 zkSync 模块。您会注意到左侧会出现一个新的 zkSync 选项卡。

该模块由用于在 zkSync 上编译、部署和与合约交互的部分组成。
17.2 编译
让我们先SimpleStorage.sol点击"编译"按钮来编译文件。
👀❗重要提示: 请确保合约中的Solidity 编译器版本与++zkSync 编译器要求++ 相符。截至录制时,所需版本为
0.8.24。
17.3 部署
编译完成后,您可以前往environment tab连接您的 MetaMask 钱包,并确保其已设置为zkSync Sepolia 测试网 。连接成功后,即可部署并验证合约SimpleStorage。
https://updraft.cyfrin.io/solidity/1-simple-storage/15-zksync-plugin/wallet.png
17.4 验证部署
点击部署按钮后,MetaMask 会请求签名。请批准签名,稍等片刻后,将显示详细的部署状态信息。如果终端输出显示绿色的"验证成功"消息,则表示您的合约已成功部署并验证。
17.5 检查部署情况
要查看我们的部署情况,您可以复制合约地址并将其粘贴到++zksync Sepolia 浏览器++中。在这里,您可以查看合约详情。
👀❗重要提示: 录制时,zkSync 插件存在一个小 bug。请参考第 14 课。
17.6 结论
做得好!您已成功将智能合约部署到 zkSync 测试网,这标志着您取得了显著成就,也是您开发历程中的重要一步。
第18节:Zksync插件修复
18.1 zkSync Remix 插件的小错误
请确保您的SimpleStorage.sol智能合约已部署在 Remix 上。然后,您可以安装 zkSync 插件并编译文件。但是,即使编译成功,部署选项卡仍会显示该消息*no smart contracts ready for deployment*。
这个问题是由于插件的一个小 bug 导致的,该 bug 要求您的智能合约必须位于一个contracts文件夹内。要解决此问题,您可以创建一个名为"contracts"的新文件夹,并将您的智能合约移动到该文件夹中。然后,您可以重新编译合约,应该就可以顺利部署了。
第19节:Zksync交互
在 zkSync 模块部分
transactions,你会找到用于调用诸如`set( )` 、` set()` 、`set()`、`set()` 和`set()`SimpleStorage等函数的按钮。点击蓝色按钮会在终端中显示输出,而橙色按钮用于存储值。addPersonlistOfPeoplenameToFavoriteNumberretrievestore

例如,当您点击橙色store按钮并输入数字时77,MetaMask 会提示您确认。确认后,您可以点击retrieve查看已存储的值。您可以随意尝试这些功能,但请注意,测试网有时可能会比较慢。
第20节:分享
恭喜!你应该为自己感到骄傲,web3 社区也为你感到骄傲。如果你有 Twitter 账号(或者创建一个),我们非常期待收到你的消息。
在本课程的 GitHub 代码库中,找到++"Tweet Me"++部分。点击此链接将打开一条推文,鼓励您分享成功将第一个合约部署到 L2 平台的喜悦。
第21节:章节回顾
21.1 介绍
在本节中,我们将快速总结第 1 课到第 9 课的内容,并了解 EVM 和与 EVM 兼容的区块链。
21.2 EVM
EVM 代表以太坊虚拟机 ( Ethereum Virtual Machine )。它是一个去中心化的计算引擎,用于执行智能合约。 任何用 Solidity 编写的合约都可以部署到任何与 EVM 兼容的区块链上。此类区块链和 Layer 2 解决方案的例子包括Ethereum、Polygon、Arbitrum、Optimism和ZKsync。
🚧警告:虽然像 ZKsync 这样的区块链可能与 EVM 兼容,但务必验证所有 Solidity 关键字是否都受支持。
21.3 Contract Setup
在编写任何智能合约之前,请务必指定您要使用的 Solidity 版本。此外,请在文件顶部包含 SPDX 许可证标识符。
bash
// SPDX-License-Identifier: MIT pragma solidity >=0.7.0 <0.9.0;
接下来,创建一个合约对象。在 Solidity 中,合约对象contract类似于其他编程语言中的类。花括号内的所有内容都{}属于合约的作用域。
21.4 类型和结构
Solidity 支持多种基础类型(例如 uint256 和 bool),允许通过 struct 创建自定义类型,并且支持数组和映射。
21.5 功能和行为
Solidity 中的函数能够修改区块链状态并执行交易。不修改区块链状态的函数则使用 view 或 pure 关键字进行声明。
21.6 数据位置和内存
Solidity 允许您为字符串、结构体和数组变量指定不同的数据位置。术语 calldata 和 memory 表示仅存在于函数调用期间的临时变量;反之,storage 变量是永久性的,会一直保留在合约中。函数参数不能是 storage 变量,因为它们仅在函数调用期间存在。
编译智能合约时,Solidity 代码会被转换为与 EVM(以太坊虚拟机)兼容的字节码,即机器可读的代码。
21.7 结论
掌握 Solidity 的基础知识(包括合约设置、数据管理和函数行为等)能为开发强大的去中心化应用(dApps)奠定坚实基础。这些基础知识对于应对区块链技术的复杂性并充分利用其潜力至关重要。做得好!
21.8 测试一下自己
🏆 尝试回答第一课的所有问题,然后再返回完成所有编程任务。