remix编译运行solidity8

学习:

mapping, nested mapping

struct + array

枚举 enum

实践:

写一个“小型会员系统”,存储用户地址、积分、等级。

用 require/revert/assert 做条件检查。

vip.sol

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SimpleLoyalty {
struct Member {
uint256 points;
uint8 level;
bool exists;
}

mapping(address => Member) public members;

event PointsAdded(address indexed user, uint256 points);
event PointsSpent(address indexed user, uint256 points);
event LevelUp(address indexed user, uint8 newLevel);

// 注册会员
function register() external {
require(!members[msg.sender].exists, "Already registered");
members[msg.sender] = Member(0, 1, true); // 初始等级1,积分0
}

// 增加积分
function addPoints(address user, uint256 pts) external {
require(members[user].exists, "User not registered");
members[user].points += pts;
emit PointsAdded(user, pts);

// 自动升级
_checkLevelUp(user);
}

// 消耗积分
function spendPoints(uint256 pts) external {
require(members[msg.sender].exists, "Not registered");
require(members[msg.sender].points >= pts, "Not enough points");
members[msg.sender].points -= pts;
emit PointsSpent(msg.sender, pts);
}

// 内部升级逻辑
function _checkLevelUp(address user) internal {
uint8 newLevel = uint8(members[user].points / 100 + 1); // 每100积分升一级
if(newLevel > members[user].level){
members[user].level = newLevel;
emit LevelUp(user, newLevel);
}
}
}

第一步点击注册register

注册成功

第二步点击addpoints,输入eth地址和添加的分数15
成功添加分数15点

第三步,查询用户分数,输入eth地址。显示26分,用户等级1,存在该用户。

uint256 = 无符号整数,最大值非常大

问题一: 在以上代码中没有搜索到storage关键字,那么是如何永久存储会员用户的等级、积分和是否注册的呢?看起来似乎存在struct里,那么struct和storage/memory/calldata有什么区别呢?

1
2
3
4
5
struct Member {
uint256 points;
uint8 level;
bool exists;
}

members 是 mapping,本身存储在 storage(链上状态)
在 Solidity 中,state variable(合约状态变量)默认是 storage,所以即使没有显式写 storage,它仍然是永久存储。

struct Member 只是容器,存放在 storage → 永久保存积分/等级/注册状态

vip2.sol (带 require / revert / assert)

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract LoyaltySystem {
struct Member {
uint256 points;
uint8 level;
bool exists;
}

mapping(address => Member) public members;

// 自定义错误(revert 专用)
error NotRegistered();
error AlreadyRegistered();
error InvalidPoints();

// 注册
function register() external {
// require:用于正常业务逻辑检查
require(!members[msg.sender].exists, "Already registered");

members[msg.sender] = Member(0, 1, true);
}

// 加积分(使用 require + revert)
function addPoints(uint256 amount) external {
// require:最常用的检查
require(amount > 0, "Invalid points");

// revert:更 gas 省,推荐用于 "不满足条件时抛错"
if (!members[msg.sender].exists) {
revert NotRegistered();
}

// storage 引用
Member storage m = members[msg.sender];

m.points += amount;

// 升级等级逻辑(业务示例)
if (m.points >= 1000) {
m.level = 2;
}
if (m.points >= 2000) {
m.level = 3;
}
}

// 获取积分
function getPoints(address user) external view returns (uint256) {
if (!members[user].exists) revert NotRegistered();
return members[user].points;
}

// assert 用法示例(内部不变量检查)
function internalCheck() internal view {
// assert 只用于“永远不应该失败”的情况
// 例如:struct 中 existstruelevel 永远必须 >=1
if (members[msg.sender].exists) {
assert(members[msg.sender].level >= 1);
}
}

// 调用 internalCheck 的示例函数
function debugCheck() external view {
internalCheck();
}
}

测试

点击两次,第一次显示注册成功,第二次提示注册失败因为已经注册了。

你问的重点:require / revert / assert 使用区别

1️⃣ require — 最常用的条件检查

用于:

输入参数是否合法

用户是否注册

用户是否有权限

金额是否足够

错误信息会以 string 形式返回。

require(amount > 0, “Invalid points”);

审计视角:
“是否所有关键操作都有必要的 require 检查”

2️⃣ revert — 更专业、gas 更便宜

当逻辑复杂时,推荐使用 自定义错误 + revert:

1
2
3
if (!members[msg.sender].exists) {
revert NotRegistered();
}

优点:

gas 消耗比 require 少 40% 以上

错误码可以精准定位,不依赖 string

审计视角:
“是否应将 require(string) 改为 revert ErrorName() 以减少 gas”

3️⃣ assert — 不应被业务触发

assert 只用于:

内部不变量(invariants)

永远不应该失败的分支

EVM bug 检查(极少用)

例如:

assert(members[msg.sender].level >= 1);

如果断言失败 → 整个交易失败,消耗所有 gas
因此不能用 assert 做输入检查!

审计视角:
“assert 是否用在了不应该使用的地方,有没有 DoS 风险”

🔍 为什么之前的合约没使用 revert / assert?

因为:

require 已经足够覆盖大部分业务逻辑

新手更容易理解

要讲解成员系统核心逻辑,不想干扰你理解

但你现在已经进入 进阶开发 / 审计思维,所以我们升级到专业写法。

enum_vip.sol

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
pragma solidity ^0.8.20;

contract Loyalty {
enum Level { Bronze, Silver, Gold, Diamond }

struct Member {
uint256 points;
Level level;
bool exists;
}

mapping(address => Member) public members;

// 注册
function register() external {
require(!members[msg.sender].exists, "Already registered");
members[msg.sender] = Member(0, Level.Bronze, true);
}

// 加积分并自动升级
function addPoints(uint256 amount) external {
require(amount > 0, "Invalid points");
Member storage m = members[msg.sender];
require(m.exists, "Not registered");

m.points += amount;

// 自动升级逻辑
if (m.points >= 10000) {
m.level = Level.Diamond;
} else if (m.points >= 5000) {
m.level = Level.Gold;
} else if (m.points >= 1000) {
m.level = Level.Silver;
}
}
}

✅ 1. 什么是 enum(枚举)?

枚举(enum)是用来表示一组有限的、可读性更高的常量。

换句话说:

  • 它让你的代码更容易理解
  • 不需要记数字,直接用名字表示状态
  • 通常用于“状态机”或“等级系统”

🧊 2. 举个简单例子

比如会员等级:

1
enum Level { Bronze, Silver, Gold, Diamond }

这会自动映射为:

名称 数字值
Bronze 0
Silver 1
Gold 2
Diamond 3

这样,你就不需要记住 0/1/2/3,代码更清晰。


🧩 3. 在会员系统里如何使用?

结构体里直接放 enum:

1
2
3
4
5
struct Member {
uint256 points;
Level level;
bool exists;
}

升级等级时:

1
2
3
4
5
if (m.points >= 1000) {
m.level = Level.Silver;
} else if (m.points >= 5000) {
m.level = Level.Gold;
}

是不是比用数字 1, 2, 3 可读性高很多?


🔐 4. enum 还能用于“任务系统 / 状态机”

比如像 Galxe / Zealy 的任务:

1
enum TaskStatus { NotStarted, InProgress, Completed, RewardClaimed }

⚙ 5. enum 是如何存储的?

enum 在底层是一个 uint8(如果状态数少于 256),也就是非常省 gas。

例如:

1
Level level;

1
uint8 level;

本质上没有区别,但可读性高得多。


⚠ 6. 审计思维:enum 的风险点

审计时会看几个点:

① 是否有 default state(默认状态)逻辑漏洞

enum 默认值是 0(第一个成员)
如果没处理这个默认值 → 漏洞风险

② 是否可能被错误升级跳级

例如 Gold 直接改成 Diamond,而中间没有检查
(状态机必须确保不可逆步骤按顺序执行)

③ 是否有 unreachable states(永远不会发生的状态)

这通常代表业务逻辑没写完整

🏁 总结一下 enum 是什么:

你要记住的重点 解释
enum = 取固定值的变量 Bronze/Silver/Gold
人类可读性高 替代数字 0/1/2/3
内部存储是 uint8 省 gas
非常适合会员等级 & 状态机 会员、任务、订单、权限
审计会重点检查状态机 是否跳级、默认状态是否安全

如果你愿意,我可以帮你把 enum 用在:

  • 会员等级系统升级为更高级状态机
  • 任务系统(Todo → Done → Rewarded)
  • NFT 会员卡(Active / Expired / Blacklisted)

你想升级哪个?

enum是不是全局变量?状态机是什么意思?

枚举 enum 是 Solidity 中非常重要、但又很容易被忽略的一个数据类型。
在会员系统(Loyalty System)里,它非常常用,尤其用于表示等级、任务状态、角色身份、会员等级状态机等。

我来一步一步讲清楚。

你的两个问题非常关键,理解之后你会真正进入 Solidity / Web3 合约开发的“中级水平”。

我用最清晰、最直观的方法回答你。


🔹 1. enum 是全局变量吗?

不是。
enum 不是“变量”,它是 一种类型(Type)

就像:

  • uint256 是一种类型
  • bool 是一种类型
  • struct 是一种类型
  • enum 也是一种类型

所以 enum 只是“定义一个新的类型”,并不会占用存储空间,也不是变量。

例子:

1
enum Level { Bronze, Silver, Gold }

这行代码的含义是:

“我创建了一种新的类型叫 Level,它的值范围是 Bronze / Silver / Gold。”

实际变量是 struct 里的字段:

1
Level public userLevel;

这个 变量 才会被储存在 storage 里。


🔹 2. 什么是“状态机”(State Machine)?

这是一个 Web3 和 Solidity 非常核心的概念。

区块链合约天然是一个“状态管理器”,状态在函数调用后发生变化。


💡 状态机的核心思想:

一个对象只能处于 有限的几个确定状态之一,并且只能按照设定规则从一个状态“合法地移动”到另一个状态。

你可以把它想象成“一步一步走”的流程。


🔍 来看例子:用 enum 实现状态机

比如会员任务系统(类似 Galxe / Zealy):

1
enum TaskStatus { NotStarted, InProgress, Completed, RewardClaimed }

一个任务的执行流程只能是:

1
2
3
4
5
6
7
NotStarted

InProgress

Completed

RewardClaimed

你不能跳步骤,例如:

  • 不能直接从 NotStarted 变成 Completed
  • 不能重复从 Completed 进入 InProgress
  • 不能从 RewardClaimed 再走回去

这就是状态机的核心:

“状态必须按规则合法迁移,不能乱跳。”


🧊 进一步理解(现实类比)

像生活中很多东西都是“状态机”:

🚦 红绿灯:
1
RedGreenYellowRed ...
🛒 订单系统:
1
Pending → Paid → Shipped → Delivered → Completed
🎮 游戏人物生命状态:
1
Alive → Dead
📱 App 登录流程:
1
LoggedOut → LoggingIn → LoggedIn

🛠️ 在 Solidity 中,状态机通常由 enum 实现

例如:

1
enum MembershipStatus { None, Active, Suspended, Banned }

或 NFT 生命周期:

1
enum NFTState { Minted, Listed, Sold, Burned }

🔐 为什么 Web3 合约非常依赖状态机?

因为合约是公开的,任何用户都可以调用函数,如果没有状态机:

❌ 用户可以重复领取奖励
❌ 用户可以跳过检查直接升级
❌ 用户可以无限 mint NFT
❌ 用户可以绕过权限检查

所以审计人员重点关注:

“状态是否按顺序正确变化?有没有办法跳级?有没有永远不能进入或永远不能离开的状态?”


📌 总结

概念 简短解释
enum 是类型 (type) 不是变量,不是全局变量,用来表达“有限状态”
enum 的变量 才会存储在 storage 里
状态机 (state machine) 对象只能按照规则在有限状态之间合法跳转
为何重要 防止作弊、越权、重复领取奖励、任务跳过等 Web3 漏洞

如果你愿意,我可以带你:

✔ 用 enum 给你的会员系统加:
  • Bronze → Silver → Gold → Diamond 的升级状态机
  • 审计检查点
  • 状态转换图(流程图)

你要看看会员系统“状态机版本”吗?