【更新中】Web3 CTF 从入门到入土
从我复读考上大学以后继续打 CTF,我就发现了跟我以前打 CTF 相比,现在的 CTF 的 Misc 中加入了更多的新题,而其中就有 Web3 这一分支。而我在网上冲浪的时候发现 Web3 的教程还是太少了(毕竟这东西太新了),于是我决定写一篇 Web3 的各方面的文章,包含所有我会遇到的东西,不仅是我自己能看,其他 Web3 小萌新也可以进行学习,于是就有了本文
因为我个人是写文章是习惯性地尽可能详细的(特别是技术文),所以如果觉得本文啰嗦的话,可以跳着看,目录在右边的公告栏下方
我会根据我自己学习的进度来更新本文章,这是长线作战,我会尽量坚持下来的
本文将会使用钱包 0xF126dCA69E9c9E5f128bb718b98f3544F9A8b413
作为测试用户
My wallet address: 0x8888813Cb0Dc768CDd0D9b3e62674E715a13d611
因为本站使用的代码高亮 highlight.js 对 Solidity 的支持是战五渣级别的,为了良好的阅读体验,对于 Solidity 代码,我会使用下面这个工具进行高亮后,再以 html 代码的形式复制到本页面中,已经通过写 js 和 css 的方式解决了没有行号和复制按钮的问题,除了看起来跟其他代码框有那么一点点的区别外,大致不影响(因此带来了没有一键复制和没有行号显示的问题),不便之处,尽请谅解(滑跪)
- Syntax Highlighter: https://syntaxhighlighter.app/zh-Hans/solidity
前置准备与知识点
入门这一节我会讲述需要准备什么东西,怎么准备,如果你已经准备好了的话可以跳过这一节
准备一个钱包
作为一名即将步入 Web3 领域的 CTFer,你需要准备一个钱包。鉴于 MetaMask 这个钱包的知名以及使用之广泛,我建议你安装一个 MetaMask
在电脑上安装
在电脑上,MetaMask 是一个浏览器插件,所以你可以使用任意 Chromium 内核的浏览器进行安装(或者是 Firefox),这里罗列几个地址
- Edge: https://microsoftedge.microsoft.com/addons/detail/metamask/ejbalbakoplchlghecdalmeeeajnimhm?hl=zh-CN
- Chrome: https://chromewebstore.google.com/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn
- Firefox: https://addons.mozilla.org/en-US/firefox/addon/ether-metamask/
- Opera: https://addons.opera.com/en-gb/extensions/details/metamask-10/
在手机上安装
如果是使用 Android 手机,可以直接通过 Google Play 进行安装,当然你也可以使用 Github
苹果手机的话你肯定绕不开 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
主要注意地址是否正确就行了,不正确的地址可能会造成你钱货两空(额……货是什么?我不道啊)
- 在线IDE:https://remix.ethereum.org/
- 本地IDE(离线版):https://github.com/remix-project-org/remix-desktop-insiders
使用钱包去接水
在每个区块链上,通常都存在多个网络,其中最重要的是主网(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 | $ npx hardhat init |
hardhat 会给我们提供测试的钱包,但是需要注意的是,不要在主网上往这些钱包转账!这些账户的私钥是公开的,所以可能会导致你的钱被盗走
开启后自己在小狐狸钱包里面加入自己的本地网络就可以了
使用 Remix IDE 部署合约
在 Remix IDE 中,我们先新建一个文件,文件名你喜欢就好
我们添加以下代码(一个简单的加法)
// SPDX-License-Identifier: MITpragma solidity >=0.7.0;
contract TestAdd { function add (uint256 _a, uint256 _b) public pure returns (uint256) { return _a + _b; }}
完成后按下 Ctrl + S 会自动编译,或者你也可以点击左侧的 Solidity 编译器
,然后点击那个蓝色的编译按钮也可以
完成编译后我们要进行部署,我们点到左边的 部署 & 发交易
选项卡,在这里点击上面的环境,选择 WalletConnect
连接自己的钱包,注意网段别选错了,万一选到了以太坊那是真给钱了
这里会让你授权部署合约,再次确认你的网络和账户是正确的
连接好后点击下面的部署就可以部署合约了,等到它部署完成就可以调用了
在部署的时候,下面的 Terminal 里面会有信息的
这里会给你显示合约详细信息的查看连接,例如我这里就是这两个
- View on Blockscout: https://eth-sepolia.blockscout.com/tx/0xee0efde8c7fb84304a3c785becc3c6e4559b8cc9bc37f5eb920bedfdc81e1891
- View on Etherscan: https://sepolia.etherscan.io/tx/0xee0efde8c7fb84304a3c785becc3c6e4559b8cc9bc37f5eb920bedfdc81e1891
这里显示这个合约的状态,部署完成后在已部署的合约中是有你的合约的,你就可以直接调用了
如果你做到了这一步,恭喜你,你已经完成你的第一次合约部署了!
与 Remix IDE 共享本地文件
我们在 Remix IDE 中编写的文件是保存在这个 IDE 里面的,但是我们很多时候还是希望保存在自己电脑上(作为文件),或者把自己电脑上的文件共享到这个 IDE 里面,这时候我们需要用到 remixd,先确保你安装了 nodejs 和 npm,然后我们来安装一下 remixd
1 | npm install -g @remix-project/remixd |
安装完成后,使用下面的命令来共享文件夹
1 | $ remixd -s .\contracts\ -u https://remix.ethereum.org/ |
你应该将这里的 .\contracts\
更换为你自己的文件夹路径,命令的用法为 remixd -s <path-to-the-shared-folder> -u <remix-ide-instance-URL>
我们还需要在 Remix IDE 里面安装 REMIXD 插件,在左下角有个插头图标,点击后我们在搜索框搜索 REMIXD
就可以找到了
启用后回到文件管理,就可以看到工作空间多了个 localhost,这里面就是你的文件了
理解以太坊
现在,是时候了解一下到底什么是以太坊,什么是 Web3 了
这个问题讲起来比较麻烦,你可能需要阅读下面的这些连接
- 什么是 Web3?(MetaMask): https://learn.metamask.io/zh-CN/lessons/what-is-web3
- 了解以太坊 (ethereum): https://ethereum.org/zh/learn/
- 以太坊(ETH)到底是什么?(通俗的解释): https://zhuanlan.zhihu.com/p/390884462
注:此账户其他的文章建议不看,AI 水分很大,但是就上面这个来说确实很形象了
其中小狐狸的教程比较简单易懂,也比较浅显,而以太坊官方的教程更加的详细
但如果你想寻求一下类比的东西,那么你可以看看下面这一段 Grok 3 的概括
以太坊是什么?
以太坊(Ethereum)就是一个去中心化的“超级计算机”平台,简单来说,它是一个基于区块链技术的网络,任何人都可以在上面运行程序、创建应用,甚至发行自己的数字货币。它不像比特币只是用来转账和存储价值,以太坊更像一个可以编程的“世界计算机”,支持各种复杂的功能。
想象一下:以太坊就像一个巨大的共享电脑,全世界的人都可以用它来写代码、跑程序,这些程序还能自动执行,不需要中间人,也不怕有人作弊。这就是它的核心魅力。
以太坊里的专有名词解释
下面咱们把一些常见的专业名词拆解一下,用人话讲明白:
- 区块链(Blockchain)
这是以太坊的基础技术。想象一个超级安全的账本,所有的交易和操作记录都写在上面,每一页(区块)都连在一起(链)。这个账本是公开的,谁都可以看,而且一旦写进去就改不了,保证了数据的安全和透明。- 以太币(Ether, 简称ETH)
这是以太坊网络里的“货币”,用来支付运行程序或交易的费用。就像你用电脑要交电费一样,在以太坊上跑程序或转账得花ETH。- 智能合约(Smart Contract)
这是以太坊的核心功能之一。智能合约就是一段自动执行的代码,写在区块链上。比如,你和朋友约定“如果明天下雨,我就给你100块”,这个条件写成代码后,到了明天如果真下雨,系统会自动转账,不需要你手动操作,也不用担心对方赖账。- 去中心化应用(DApp)
这是运行在以太坊上的应用程序,和咱们手机上的App差不多,但区别在于它不是由某个公司控制,而是跑在区块链上,数据公开,规则透明。比如去中心化的金融应用(DeFi)、游戏、投票系统等。- Gas(燃气费用)
在以太坊上运行任何操作(比如转账或执行智能合约)都需要支付手续费,这个费用叫Gas。Gas是用ETH支付的,类似于你开车要加油,Gas费高低取决于网络的“拥堵程度”和操作的复杂性。- 节点(Node)
节点就是参与以太坊网络的电脑。任何人都可以运行一个节点来帮助维护网络,记录交易数据。节点越多,网络越安全、越去中心化。- 挖矿(Mining) 和 PoW/PoS
- 挖矿:早期以太坊通过“挖矿”来验证交易和生成新区块, miners(矿工)用电脑算力解决问题,成功后就能获得ETH奖励。这叫工作量证明(PoW,Proof of Work),就是比特币用的那套。
- 现在以太坊已经升级到“合并”(The Merge),改用了权益证明(PoS,Proof of Stake)。不再靠算力,而是靠持有和质押ETH来参与验证交易,更加节能。
- 以太坊虚拟机(EVM)
这是以太坊的核心引擎,负责执行智能合约代码。就像你的电脑运行软件一样,EVM是所有DApp和智能合约的“运行环境”。- 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: MITpragma 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 | class Dog: |
在这里的 Python 代码中,我们定义了一个名为 Dog
的对象,并且定义了它的一个函数叫做 bark
(吠叫),当我们使用它的时候,肯定是这样用的
1 | dog = Dog() |
这会输出 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
表示这个变量counter
为uint256
类型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 | class AddCalculator: |
我们对 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); }}
在这里我们对 save
和 calculateAndSave
加入了 private
修饰符,于是在计算并保存的函数里我们对 save
进行调用是合法的,但是在下面那个派生合约里面对 calculateAndSave
调用就是不合法的,直接上手就是编译不通过
1 | DeclarationError: Undeclared identifier. |
定义变量与常量
Solidity 不像 Python 那样,它更类似于 C,是一门静态语言,所以对于变量,必须使用前定义,定义时声明类型,在上面的例子中,你或许见过了 uint
/uint256
uint num = 0;
这里的 uint
与 uint256
是等价的,它是一个无符号的 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
会编译出错,因为右值不确定