从我复读考上大学以后继续打 CTF,我就发现了跟我以前打 CTF 相比,现在的 CTF 的 Misc 中加入了更多的新题,而其中就有 Web3 这一分支。而我在网上冲浪的时候发现 Web3 的教程还是太少了(毕竟这东西太新了),于是我决定写一篇 Web3 的各方面的文章,包含所有我会遇到的东西,不仅是我自己能看,其他 Web3 小萌新也可以进行学习,于是就有了本文

因为我个人是写文章是习惯性地尽可能详细的(特别是技术文),所以如果觉得本文啰嗦的话,可以跳着看,目录在右边的公告栏下方

我会根据我自己学习的进度来更新本文章,这是长线作战,我会尽量坚持下来的

本文将会使用钱包 0xF126dCA69E9c9E5f128bb718b98f3544F9A8b413 作为测试用户

My wallet address: 0x8888813Cb0Dc768CDd0D9b3e62674E715a13d611

因为本站使用的代码高亮 highlight.js 对 Solidity 的支持是战五渣级别的,为了良好的阅读体验,对于 Solidity 代码,我会使用下面这个工具进行高亮后,再以 html 代码的形式复制到本页面中,已经通过写 js 和 css 的方式解决了没有行号和复制按钮的问题,除了看起来跟其他代码框有那么一点点的区别外,大致不影响(因此带来了没有一键复制和没有行号显示的问题),不便之处,尽请谅解(滑跪)

前置准备与知识点

入门这一节我会讲述需要准备什么东西,怎么准备,如果你已经准备好了的话可以跳过这一节

准备一个钱包

作为一名即将步入 Web3 领域的 CTFer,你需要准备一个钱包。鉴于 MetaMask 这个钱包的知名以及使用之广泛,我建议你安装一个 MetaMask

在电脑上安装

在电脑上,MetaMask 是一个浏览器插件,所以你可以使用任意 Chromium 内核的浏览器进行安装(或者是 Firefox),这里罗列几个地址

在手机上安装

如果是使用 Android 手机,可以直接通过 Google Play 进行安装,当然你也可以使用 Github

https://github.com/MetaMask/metamask-mobile/releases

苹果手机的话你肯定绕不开 App Store

https://apps.apple.com/us/app/metamask-crypto-wallet/id1438144202

安装完成后初始化一个钱包就行了,当然了,以太坊主网有点难连接上,可以考虑开个梯子

安全提示:请不要把你的助记词/私钥发给任何人;请只授权你信任的 DAPP/网站与你的钱包进行连接!

不管你对 Web3 有多了解,如果你看到这里了,我都建议你再去看一次小狐狸的安全指南:https://learn.metamask.io/zh-CN/lessons/security-in-web3

准备 Remix IDE

Remix IDE 是一个开源的、基于浏览器的集成开发环境(IDE),主要用于开发、编译、测试和部署以太坊(Ethereum)区块链上的智能合约。它由以太坊基金会支持,旨在为开发者提供一个简单易用的工具来编写 Solidity 语言(以太坊智能合约的主要编程语言)代码。—— Grok

主要注意地址是否正确就行了,不正确的地址可能会造成你钱货两空(额……货是什么?我不道啊)

使用钱包去接水

在每个区块链上,通常都存在多个网络,其中最重要的是主网(Mainnet)和测试网(Testnet)。

主网网络(Mainnet):主网网络是真正的生产环境,其中的交易使用真实的代币进行结算。这是真正的价值交换发生的地方,因此需要小心谨慎。

测试网络(Testnet):在主网上进行开发和测试可能会非常昂贵,因为每个操作都需要使用真实代币支付Gas费用。为了解决这个问题,每个区块链都提供了测试网络,这些网络上的代币没有实际价值,而且通常可以通过水龙头免费获得。开发者可以在测试网络上构建和测试智能合约,以确保它们在主网上能够正确运行。

我们在测试的过程中,当然不可能去使用真钱,所以我们要去拿测试币(俗称:接水)

我这边使用的是由 Google 提供的 Ethereum Sepolia Faucet,这里有好几种测试币可以选择,我选择在 Sepolia 上进行测试,所以我选择 Ethereum Sepolia,每次可以接到 0.05 ETH(每个 Google 账号每 24 小时可以接这个数量)

实际上,0.05 ETH 已经能干很多事情了

https://cloud.google.com/application/web3/faucet/ethereum/sepolia

把自己的钱包地址填进去就可以了,地址在钱包里面顶上复制一下就好了

(可选)部署一个最简单的开发节点

你可以使用 hardhat 来部署一个最简单的开发节点,首先你的确保你安装了 nodejs

然后使用 npx hardhat init 来初始化你的节点,使用 npx hardhat node 来开启节点的服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
$ npx hardhat init
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888

Welcome to Hardhat v2.24.0

√ What do you want to do? · Create a TypeScript project
√ Hardhat project root: · F:\CTF\Web3\Node
√ Do you want to add a .gitignore? (Y/n) · y
√ Help us improve Hardhat with anonymous crash reports & basic usage data? (Y/n) · y
√ Do you want to install this sample project's dependencies with npm (hardhat @nomicfoundation/hardhat-toolbox)? (Y/n) · y


npm install --save-dev "hardhat@^2.24.0" "@nomicfoundation/hardhat-toolbox@^5.0.0"
npm warn deprecated [email protected]: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated [email protected]: Glob versions prior to v9 are no longer supported
npm warn deprecated [email protected]: Glob versions prior to v9 are no longer supported
npm warn deprecated [email protected]: Glob versions prior to v9 are no longer supported
npm warn deprecated [email protected]: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
npm warn deprecated [email protected]: Glob versions prior to v9 are no longer supported
npm warn deprecated [email protected]: Glob versions prior to v9 are no longer supported

added 576 packages in 22s

103 packages are looking for funding
run `npm fund` for details

Project created

See the README.md file for some example tasks you can run

Give Hardhat a star on Github if you're enjoying it!

https://github.com/NomicFoundation/hardhat

$ npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Account #1: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

Account #2: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a

Account #3: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000 ETH)
Private Key: 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6

Account #4: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000 ETH)
Private Key: 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a

Account #5: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc (10000 ETH)
Private Key: 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba

Account #6: 0x976EA74026E726554dB657fA54763abd0C3a0aa9 (10000 ETH)
Private Key: 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e

Account #7: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 (10000 ETH)
Private Key: 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356

Account #8: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f (10000 ETH)
Private Key: 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97

Account #9: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 (10000 ETH)
Private Key: 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6

Account #10: 0xBcd4042DE499D14e55001CcbB24a551F3b954096 (10000 ETH)
Private Key: 0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897

Account #11: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788 (10000 ETH)
Private Key: 0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82

Account #12: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a (10000 ETH)
Private Key: 0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1

Account #13: 0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec (10000 ETH)
Private Key: 0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd

Account #14: 0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097 (10000 ETH)
Private Key: 0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa

Account #15: 0xcd3B766CCDd6AE721141F452C550Ca635964ce71 (10000 ETH)
Private Key: 0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61

Account #16: 0x2546BcD3c84621e976D8185a91A922aE77ECEc30 (10000 ETH)
Private Key: 0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0

Account #17: 0xbDA5747bFD65F08deb54cb465eB87D40e51B197E (10000 ETH)
Private Key: 0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd

Account #18: 0xdD2FD4581271e230360230F9337D5c0430Bf44C0 (10000 ETH)
Private Key: 0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0

Account #19: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH)
Private Key: 0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

hardhat 会给我们提供测试的钱包,但是需要注意的是,不要在主网上往这些钱包转账!这些账户的私钥是公开的,所以可能会导致你的钱被盗走

开启后自己在小狐狸钱包里面加入自己的本地网络就可以了

使用 Remix IDE 部署合约

在 Remix IDE 中,我们先新建一个文件,文件名你喜欢就好

我们添加以下代码(一个简单的加法)

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
contract TestAdd {
function add (uint256 _a, uint256 _b) public pure returns (uint256) {
return _a + _b;
}
}

完成后按下 Ctrl + S 会自动编译,或者你也可以点击左侧的 Solidity 编译器,然后点击那个蓝色的编译按钮也可以

完成编译后我们要进行部署,我们点到左边的 部署 & 发交易 选项卡,在这里点击上面的环境,选择 WalletConnect 连接自己的钱包,注意网段别选错了,万一选到了以太坊那是真给钱了

这里会让你授权部署合约,再次确认你的网络和账户是正确的

连接好后点击下面的部署就可以部署合约了,等到它部署完成就可以调用了

在部署的时候,下面的 Terminal 里面会有信息的

这里会给你显示合约详细信息的查看连接,例如我这里就是这两个

这里显示这个合约的状态,部署完成后在已部署的合约中是有你的合约的,你就可以直接调用了

如果你做到了这一步,恭喜你,你已经完成你的第一次合约部署了!

与 Remix IDE 共享本地文件

我们在 Remix IDE 中编写的文件是保存在这个 IDE 里面的,但是我们很多时候还是希望保存在自己电脑上(作为文件),或者把自己电脑上的文件共享到这个 IDE 里面,这时候我们需要用到 remixd,先确保你安装了 nodejs 和 npm,然后我们来安装一下 remixd

1
npm install -g @remix-project/remixd

安装完成后,使用下面的命令来共享文件夹

1
2
3
4
5
6
7
8
$ remixd -s .\contracts\ -u https://remix.ethereum.org/
[INFO] you are using the latest version 0.6.48
[WARN] You may now only use IDE at https://remix.ethereum.org/ to connect to that instance
[WARN] Any application that runs on your computer can potentially read from and write to all files in the directory.
[WARN] Symbolic links are not forwarded to Remix IDE

[INFO] Tue May 27 2025 13:01:14 GMT+0800 (香港标准时间) remixd is listening on 127.0.0.1:65520
[INFO] Tue May 27 2025 13:01:14 GMT+0800 (香港标准时间) slither is listening on 127.0.0.1:65523

你应该将这里的 .\contracts\ 更换为你自己的文件夹路径,命令的用法为 remixd -s <path-to-the-shared-folder> -u <remix-ide-instance-URL>

我们还需要在 Remix IDE 里面安装 REMIXD 插件,在左下角有个插头图标,点击后我们在搜索框搜索 REMIXD 就可以找到了

启用后回到文件管理,就可以看到工作空间多了个 localhost,这里面就是你的文件了

理解以太坊

现在,是时候了解一下到底什么是以太坊,什么是 Web3 了

这个问题讲起来比较麻烦,你可能需要阅读下面的这些连接

其中小狐狸的教程比较简单易懂,也比较浅显,而以太坊官方的教程更加的详细

但如果你想寻求一下类比的东西,那么你可以看看下面这一段 Grok 3 的概括

以太坊是什么?

以太坊(Ethereum)就是一个去中心化的“超级计算机”平台,简单来说,它是一个基于区块链技术的网络,任何人都可以在上面运行程序、创建应用,甚至发行自己的数字货币。它不像比特币只是用来转账和存储价值,以太坊更像一个可以编程的“世界计算机”,支持各种复杂的功能。

想象一下:以太坊就像一个巨大的共享电脑,全世界的人都可以用它来写代码、跑程序,这些程序还能自动执行,不需要中间人,也不怕有人作弊。这就是它的核心魅力。

以太坊里的专有名词解释

下面咱们把一些常见的专业名词拆解一下,用人话讲明白:

  1. 区块链(Blockchain)
    这是以太坊的基础技术。想象一个超级安全的账本,所有的交易和操作记录都写在上面,每一页(区块)都连在一起(链)。这个账本是公开的,谁都可以看,而且一旦写进去就改不了,保证了数据的安全和透明。
  2. 以太币(Ether, 简称ETH)
    这是以太坊网络里的“货币”,用来支付运行程序或交易的费用。就像你用电脑要交电费一样,在以太坊上跑程序或转账得花ETH。
  3. 智能合约(Smart Contract)
    这是以太坊的核心功能之一。智能合约就是一段自动执行的代码,写在区块链上。比如,你和朋友约定“如果明天下雨,我就给你100块”,这个条件写成代码后,到了明天如果真下雨,系统会自动转账,不需要你手动操作,也不用担心对方赖账。
  4. 去中心化应用(DApp)
    这是运行在以太坊上的应用程序,和咱们手机上的App差不多,但区别在于它不是由某个公司控制,而是跑在区块链上,数据公开,规则透明。比如去中心化的金融应用(DeFi)、游戏、投票系统等。
  5. Gas(燃气费用)
    在以太坊上运行任何操作(比如转账或执行智能合约)都需要支付手续费,这个费用叫Gas。Gas是用ETH支付的,类似于你开车要加油,Gas费高低取决于网络的“拥堵程度”和操作的复杂性。
  6. 节点(Node)
    节点就是参与以太坊网络的电脑。任何人都可以运行一个节点来帮助维护网络,记录交易数据。节点越多,网络越安全、越去中心化。
  7. 挖矿(Mining)PoW/PoS
    • 挖矿:早期以太坊通过“挖矿”来验证交易和生成新区块, miners(矿工)用电脑算力解决问题,成功后就能获得ETH奖励。这叫工作量证明(PoW,Proof of Work),就是比特币用的那套。
    • 现在以太坊已经升级到“合并”(The Merge),改用了权益证明(PoS,Proof of Stake)。不再靠算力,而是靠持有和质押ETH来参与验证交易,更加节能。
  8. 以太坊虚拟机(EVM)
    这是以太坊的核心引擎,负责执行智能合约代码。就像你的电脑运行软件一样,EVM是所有DApp和智能合约的“运行环境”。
  9. Layer 2(二层解决方案)
    以太坊主网(Layer 1)有时候会很慢,Gas费也很贵。所以就有了Layer 2技术,比如Optimism、Arbitrum,它们是建在以太坊上的“辅助网络”,可以处理更多交易,降低成本,然后再把结果汇总到主网。

总结一下

以太坊是一个支持智能合约和去中心化应用的区块链平台,它的“货币”是ETH,它的“燃料”是Gas。用户可以在上面开发各种应用(DApp),所有操作都由智能合约自动执行,数据存储在公开透明的区块链上。网络通过节点运行,经过PoS机制验证交易,同时还有Layer 2来优化速度和成本。

简单来说,以太坊就是一个开放的数字世界,你可以在里面创造、交易、合作,而且不需要相信任何人,因为规则和代码说了算。

学习 Solidity

我是跟着右边这个链接学习的,再加上我的一些解释/见解,共同组成了这一部分:https://decert.me/tutorial/solidity/intro/

在这一节中,我假设作为读者的你有这部分的编程基础,学过至少一门编程语言(例如 C、Python、Javascript 等)

最基础的合约文件

最简单的合约文件由两个部分组成:编译器版本声明、合约,拿上面的加法合约举例

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
contract TestAdd {
function add (uint256 _a, uint256 _b) public pure returns (uint256) {
return _a + _b;
}
}
  • pragma solidity >=0.7.0; 表示编译器版本应该大于等于 0.7.0,这个写法是与 npm 的写法兼容的,这里也可以写作 ^0.7.0
  • contract TestAdd 定义了一个合约,这个合约的名字叫做 TestAdd
    • function add (uint256 _a, uint256 _b) public pure returns (uint256) 定义了一个在 TestAdd 合约里面的函数,函数名为 add
      • (uint256 _a, uint256 _b) 与 C 类似,这里表明了这个函数需要接受两个参数 _a_b,他们都是 uint256 类型
      • public 是一个可见性修饰符,表示它能够被任何人调用(关于可见性修饰符,后面会有详细的解释)
      • pure 是一个状态可变性修饰符,表示这个函数既不会读取合约的状态变量,也不会修改合约的状态变量(关于可变性修饰符,后面会有详细的解释)
      • returns (uint256) 表示这个函数的返回值是一个 uint256 类型

于是这个名为 TestAdd 的合约最后拥有了一个给任何用户调用并输入 _a_b,不改变合约内部变量,返回 uint256 数字的一个加法函数,这也是这个合约的所有内容

看完这里,你可以先去尝试一下这个挑战:https://decert.me/quests/d99f56d8-3b98-47e5-893d-e6f63810a151

理解合约

合约可以看作是 class(一个对象),举个例子,在学习 Python 的过程中,你肯定遇到过这样的一个例子

1
2
3
4
5
6
class Dog:
def __init__(self):
...

def bark(self, sound="know-wow"):
return sound

在这里的 Python 代码中,我们定义了一个名为 Dog 的对象,并且定义了它的一个函数叫做 bark(吠叫),当我们使用它的时候,肯定是这样用的

1
2
dog = Dog()
print(dog.bark(sound="know-wow"))

这会输出 know-wow,而我们在 Solidity 中,是这样写的

pragma solidity ^0.8.0;
contract Dog {
function Bark (string calldata _sound) public pure returns (string calldata) {
return _sound;
}
}

当我们部署合约并调用时,传入 know-wow,它也会返回 know-wow(图示左下角)

变量与函数的可见性

在上面,我们都没有真正定义一个变量,接下来,我们先看这个合约(一个经典的计数器合约)

pragma solidity >=0.8.0;
contract Counter {
uint public counter;
constructor() {
counter = 0;
}
function increase() public {
counter += 1;
}
function decrease() public {
counter -= 1;
}
function get() view public returns (uint) {
return counter;
}
}

在这里,我们可以发现在合约 Counter 中,我们新建了一个 uint 类型的 public 变量 counter,并在 constructor() 函数中让 counter 的值设置为 0

  • uint public counter
    • uint 表示这个变量 counteruint256 类型
    • public 表示 counter 可以被公开访问

类似于 C,Solidity 还有其他的可见性修饰符(大部分都可以用在变量/函数上,特殊说明除外)

外部修饰符:external

不能用在变量上,只能用在函数上,声明为 external 的函数只能在外部进行调用,此时称这些函数为外部函数

当我们在一个合约的函数中添加了 external 修饰符,那么在这个合约中就不能对这个函数进行直接调用

我们在这里创建一个计算器

pragma solidity ^0.8.0;
contract Calculator {
uint Storage;
constructor() {
Storage = 0;
}
function add(uint _a, uint _b) public pure returns (uint) {
return _a + _b;
}
function minus(uint _a, uint _b) public pure returns (uint) {
return _a - _b;
}
function times(uint _a, uint _b) public pure returns (uint) {
return _a * _b;
}
function initialize() external {
Storage = 0;
}
function test_func() public {
initialize(); // 这是错误的,因为 initialize 函数具有 external 修饰符,不能在合约内部进行调用
}
}

在这里,我们给函数 initialize 添加了 external 修饰符,在 test_func 中,我们对 initialize 进行了调用,我们的编译器会告诉我们这里有问题

DeclarationError: Undeclared identifier. “initialize” is not (or not yet) visible at this point.

这里就是告诉我们 initialize 在合约内部是不可见的,如果我们实在要对 initialize 在合约内部进行调用,我们应该采用 this.initialize()

pragma solidity ^0.8.0;
contract Calculator {
...
function test_func() public {
this.initialize(); // 改成 this.initialize 就可以了
}
}

采用 this.initialize() 后,会发起一个从外部调用到合约自身的一次调用,但是,这会消耗更多的 Gas(给的钱更多了)

公共修饰符:public

通过 public 修饰的变量或者函数,它们既可以在合约内部进行访问,也可以在合约外部进行方式(以接口形式,在其他合约或者链下都可以对其进行调用)

对于 public 修饰的变量,会自动创建一个同名的访问器(可以理解为 __getter__),用来获取这个变量的值

pragma solidity ^0.8.0;
contract ContractA {
uint256 public varA;
constructor() {
varA = 0;
}
function AddVarA() public {
varA += 1;
}
}
contract ContractMain {
ContractA public instance; // 初始化一个 ContractA 合约
constructor(address _address) {
instance = ContractA(_address); // 通过传入 ContractA 合约所在的地址来初始化实例
}
function trigger() public returns (uint256) {
instance.AddVarA(); // 调用实例的 AddVarA 函数,进行 varA 的自增
return instance.varA(); // 调用实例的 varA 函数(这是一个自动生成的访问器函数),得到 varA 的值
}
}

通过 REMIX IDE 我们也能够体会到这一个过程,我们先对 ContractA 进行部署后,再对 ContractMain 进行部署

子合约/派生合约修饰符:internal

使用 internal 修饰的函数/变量只能在当前合约调用或者其派生合约/子合约中使用,首先先简单说明一下派生合约,以下面这段代码为例

pragma solidity ^0.8.0;
contract AddCalculator {
uint Storage;
constructor() {
Storage = 0;
}
function add(uint _a, uint _b) public pure returns (uint) {
return _a + _b;
}
function save(uint _num) public {
Storage = _num;
}
}
contract AddAndMinusCalculator is AddCalculator {
function minus(uint _a, uint _b) public pure returns (uint) {
return _a - _b;
}
}

在这里,我们有一个 AddCalculator 的合约,它只有加法和一个存储功能,在 AddAndMinusCalculator 中,我们使用了关键词 is 来继承 AddCalculator 的属性和函数,并给它添加了减法,也就是说,只看 AddAndMinusCalculator 的情况下,它等同于

pragma solidity ^0.8.0;
contract AddAndMinusCalculator {
uint Storage;
constructor() {
Storage = 0;
}
function add(uint _a, uint _b) public pure returns (uint) {
return _a + _b;
}
function save(uint _num) public {
Storage = _num;
function minus(uint _a, uint _b) public pure returns (uint) {
return _a - _b;
}
}

你可以把这个看作是 Python 那样的类继承,上面这个与下面这段 Python 代码是类似的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AddCalculator:
def __init__(self):
self.Storage = 0

def add(self, a, b):
return a + b

def save(self, num):
self.Storage = num

class AddAndMinusCalculator(AddCalculator):
def minus(self, a, b):
return a - b

if __name__ == "__main__":
calculator = AddAndMinusCalculator()

我们对 AddAndMinusCalculator 进行合约部署,可以看到它确实也继承了 AddCalculator 的函数

现在回到我们的问题,标有 internal 的函数只能在其内部或者其派生合约内部进行调用

还是拿上面的计算器举例子

pragma solidity ^0.8.0;
contract AddCalculator {
uint Storage;
constructor() {
Storage = 0;
}
// 现在给 add 函数加入了 internal 修饰
function add(uint _a, uint _b) internal pure returns (uint) {
return _a + _b;
}
function save(uint _num) public {
Storage = _num;
}
// 内部函数可以调用内部函数
function calculateAndSave(uint _x, uint _y) public {
uint result = add(_x, _y); // AddCalculator 内部调用 add
save(result);
}
}
contract AddAndMinusCalculator is AddCalculator {
function minus(uint _a, uint _b) public pure returns (uint) {
return _a - _b;
}
// 派生合约可以调用基合约的 internal 函数
function addAndThenMinus(uint _a, uint _b, uint _c) public pure returns (uint) {
uint sum = add(_a, _b); // AddAndMinusCalculator 内部调用继承来的 add
return minus(sum, _c);
}
// 派生合约也可以直接提供一个 public 函数来调用这个 internal 函数
function externalAdd(uint _a, uint _b) public pure returns (uint) {
return add(_a, _b); // 通过 externalAdd 间接对外暴露 add 功能
}
}
contract OutsideCalculator {
AddCalculator instance;
AddAndMinusCalculator instance1;
constructor(address _add_calculator_address, address _add_and_minus_calculator_address) {
instance = AddCalculator(_add_calculator_address);
}
function add(uint _a, uint _b) public pure returns (uint) {
return instance.add(_a, _b); // 这里是非法的,因为 instance.add 是一个 internal 函数,不能在外部合约被调用,这里会编译出错
// TypeError: Member "add" not found or not visible after argument-dependent lookup in contract AddCalculator.
}
function externalAdd(uint _a, uint _b) public pure returns (uint) {
return instance1.externalAdd(_a, _b); // 这是合法的,因为 instance1.externalAdd 是一个使用 public 修饰的函数,将原来的 add 函数暴露在外了
}
}

私有修饰符:private

在这四种修饰符中,private 是最为严格的,它只允许自己使用,并且不允许派生合约使用,再来看下面这个例子

pragma solidity ^0.8.0;
contract AddCalculator {
uint Storage;
constructor() {
Storage = 0;
}
function add(uint _a, uint _b) internal pure returns (uint) {
return _a + _b;
}
// save 函数加入了 private 修饰符
function save(uint _num) private {
Storage = _num;
}
// calculateAndSave 函数加入了 private 修饰符
function calculateAndSave(uint _x, uint _y) private {
uint result = add(_x, _y);
save(result); // private 修饰允许合约内部调用
}
}
contract AddAndMinusCalculator is AddCalculator {
function minus(uint _a, uint _b) public pure returns (uint) {
return _a - _b;
}
function addAndThenMinus(uint _a, uint _b, uint _c) public pure returns (uint) {
uint sum = add(_a, _b);
return minus(sum, _c);
}
// private 修饰不允许派生合约调用
function addAndSave(uint _a, uint _b) public {
calculateAndSave(_a, _b);
}
}

在这里我们对 savecalculateAndSave 加入了 private 修饰符,于是在计算并保存的函数里我们对 save 进行调用是合法的,但是在下面那个派生合约里面对 calculateAndSave 调用就是不合法的,直接上手就是编译不通过

1
2
3
4
5
DeclarationError: Undeclared identifier.
--> privateTest.sol:38:9:
|
38 | calculateAndSave(_a, _b);
| ^^^^^^^^^^^^^^^^

定义变量与常量

Solidity 不像 Python 那样,它更类似于 C,是一门静态语言,所以对于变量,必须使用前定义,定义时声明类型,在上面的例子中,你或许见过了 uint/uint256

uint num = 0;

这里的 uintuint256 是等价的,它是一个无符号的 256 位整数,这前面的 u 可以看作 unsigned

对于一个 N 位的无符号整数,它可以表示的最小值是 0最大值是 2^N - 1

类型 位数 (N) 存储大小 (字节) 最小值 最大值 (2^N - 1) 备注
uint8 8 1 0 2^8 - 1 (即 255)
uint16 16 2 0 2^16 - 1 (即 65,535)
uint24 24 3 0 2^24 - 1 (即 16,777,215)
uint32 32 4 0 2^32 - 1 (即 4,294,967,295) C 的 int、Windows/Linux32 下 C 的 long
uint40 40 5 0 2^40 - 1
uint48 48 6 0 2^48 - 1
uint56 56 7 0 2^56 - 1
uint64 64 8 0 2^64 - 1 Linux64 下的 long
uint72 72 9 0 2^72 - 1
uint80 80 10 0 2^80 - 1
uint88 88 11 0 2^88 - 1
uint96 96 12 0 2^96 - 1
uint104 104 13 0 2^104 - 1
uint112 112 14 0 2^112 - 1
uint120 120 15 0 2^120 - 1
uint128 128 16 0 2^128 - 1
uint136 136 17 0 2^136 - 1
uint144 144 18 0 2^144 - 1
uint152 152 19 0 2^152 - 1
uint160 160 20 0 2^160 - 1 足够存储一个以太坊地址作为数字 (20 字节 = 160 位)
uint168 168 21 0 2^168 - 1
uint176 176 22 0 2^176 - 1
uint184 184 23 0 2^184 - 1
uint192 192 24 0 2^192 - 1
uint200 200 25 0 2^200 - 1
uint208 208 26 0 2^208 - 1
uint216 216 27 0 2^216 - 1
uint224 224 28 0 2^224 - 1
uint232 232 29 0 2^232 - 1
uint240 240 30 0 2^240 - 1
uint248 248 31 0 2^248 - 1
uint256 256 32 0 2^256 - 1 (~1.1579 x 10^77) EVM 原生字大小,通常是最高效的类型。
uint 256 32 0 2^256 - 1 (~1.1579 x 10^77) uint256 的别名。在不指定位数时默认使用。

这里 uint 后面的数字,其实就是 bit 数(计算机常识:1 Byte = 8 bit)

与 C 类似,定义一个变量的格式为 <type> <visibility> <name>,例如 uint public num 就是一个公开的 uint 类型的名为 num 的变量,需要注意的是,当没有对可见性添加指定,默认为 internal

合约中变量会在区块链中分配一个存储单元,这个变量是“变”的,也成为状态变量

当然了,有“变”量,就有“不可变”量,在 Solidity 中,“不可变”量有我们常说的 常量不可变量(对它的名字就叫做 不可变量,这就是为啥前面我用引号把那三个字括起来)

常量

与 C 类似但稍有不同的是,我们使用 constant 关键词来声明一个变量为常量

constant 关键词只用于修饰 strings 和任意值类型(关于值类型,下面会说到)

uint constant pi = 3.1415926;

在这里,我声明了一个名为 pi 的常量并把它的值设置为 3.1415926,常量一旦声明就不可以进行变更

并且需要注意的是:常量只能在定义的时候就进行赋值,并且等号右边的式子在编译时必须有一个确切的结果

下面我们看一个违反上面这句话的例子

uint _a = 5; // _a 是一个运行时变量
uint _b = 10; // _b 是一个运行时变量
uint constant sum = _a + _b; // 编译出错,因为在编译过程中,`_a + _b` 的结果不确定

这里可以看到,我们定义了变量 _a_b,但是这两个变量是在运行代码的时候才进行赋值的,在编译过程中,它们的值仍然是未知的,所以在第四行中的 sum = _a + _b 会编译出错,因为右值不确定