📋 学习目标
-
• 理解智能合约的概念和 EVM 工作原理
-
• 掌握 Solidity 语言的基本语法
-
• 学习 Solidity 的数据类型和变量
-
• 理解函数的可见性和修饰符
-
• 使用 Remix IDE 编写第一个智能合约
-
• 在区块链上部署并测试合约
📚 理论部分 (40分钟)
2.1 智能合约简介
什么是智能合约?
智能合约是部署在区块链上的程序代码,它自动执行预定义的规则和条款。
智能合约的特点:
-
- 不可变性 (Immutable)
-
• 一旦部署,代码无法修改
-
• 这是区块链安全的基石
-
• 部署前必须充分测试
-
- 自动执行
-
• 满足条件时自动触发
-
• 无需第三方介入
-
• 消除了人为错误和欺诈
-
- 确定性
-
• 给定相同的输入,必定产生相同的输出
-
• 在所有节点上执行结果一致
-
• 这是共识机制的基础
-
- 透明性
-
• 合约代码公开可见
-
• 任何人都可以审计
-
• 任何人都可以调用
EVM(以太坊虚拟机)
什么是 EVM?
EVM 是执行 Solidity 字节码的运行环境,类似于 Java 虚拟机 (JVM)。
EVM 的特点:
| 特性 | 说明 |
|---|---|
| 图灵完备 | 可以执行任何计算逻辑 |
| 隔离环境 | 每个合约在独立沙箱中运行 |
| Gas 限制 | 每个操作都有计算成本 |
| 无状态 | 不保存状态,每次执行从头开始 |
| 确定性 | 执行结果可预测 |
EVM 执行流程:
1. 接收交易
↓
2. 验证 Gas 费用
↓
3. 加载合约代码
↓
4. 执行字节码
↓
5. 更新状态
↓
6. 返回结果
↓
7. 消耗 Gas
为什么需要 Gas?
-
• 防止无限循环攻击
-
• 为计算资源定价
-
• 激励矿工/验证者打包交易
-
• 防止网络滥用
2.2 Solidity 语言特点
Solidity 简介
Solidity 是面向智能合约的高级编程语言,受 C++、Python 和 JavaScript 影响。
Solidity 的核心特性:
| 特性 | 说明 | 类比 |
|---|---|---|
| 静态类型 | 编译时检查类型错误 | Java、C++ |
| 面向合约 | 以合约为核心组织代码 | 面向对象编程 |
| Gas 敏感 | 每行代码都有计算成本 | 按量计费 |
| 继承支持 | 支持多重继承 | Java、Python |
| 事件驱动 | 通过 event 记录日志 | JavaScript 事件 |
| 异常处理 | require/revert/assert | try-catch |
Solidity vs JavaScript
| 特性 | JavaScript | Solidity |
|---|---|---|
| 类型 | 动态类型 | 静态类型 |
| 执行环境 | 浏览器/Node.js | EVM |
| 状态存储 | 内存/硬盘 | 区块链状态 |
| 代码可变性 | 可随时修改 | 部署后不可变 |
| 执行成本 | 免费 | 需要支付 Gas |
| 并发模型 | 单线程异步 | 单线程同步 |
Solidity 的独特概念:
-
• Gas: 每个操作都有成本
-
• Storage: 永久存储,成本高
-
• Memory: 临时存储,成本低
-
• Calldata: 只读数据,成本最低
-
• Events: 日志记录,不存储在状态中
2.3 基础语法详解
数据类型
值类型 (Value Types)
值类型在赋值时会创建副本。
// 布尔类型
bool isTrue = true;
bool isFalse = false;
// 整数类型
uint256 amount = 100; // 无符号整数 (uint = uint256)
uint8 smallNumber = 255; // 8位无符号整数 (0-255)
int256 temperature = -10; // 有符号整数
int32 shortInt = -1000; // 32位有符号整数
// 地址类型
address user = 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb;
address payable recipient = 0x123...; // 可接收 ETH 的地址
// 字节类型
bytes32 data = "hello"; // 固定大小字节数组
bytes1 singleByte = 0x01; // 单字节
// 枚举类型
enum Status { Active, Inactive, Pending }
Status currentStatus = Status.Active;
整数类型详解:
// 无符号整数
uint8 // 8位, 范围: 0 到 2^8 - 1
uint16 // 16位
uint32 // 32位
uint64 // 64位
uint128 // 128位
uint256 // 256位 (默认)
// 有符号整数
int8 // 8位, 范围: -2^7 到 2^7 - 1
int16 // 16位
int32 // 32位
int64 // 64位
int128 // 128位
int256 // 256位 (默认)
引用类型 (Reference Types)
引用类型在赋值时不会创建副本,而是引用同一数据。
// 字符串
string name = "Alice";
string description = "This is a long description...";
// 动态字节数组
bytes dynamicData = hex"01a2b3c4";
// 数组
uint[] numbers; // 动态数组
uint[10] fixedNumbers; // 固定大小数组
string[] names = ["Alice", "Bob"]; // 初始化数组
// 映射 (类似哈希表/字典)
mapping(address => uint256) balances; // 地址 → 余额
mapping(address => mapping(address => uint256)) allowances; // 嵌套映射
// 结构体
struct User {
string name;
uint256 age;
address wallet;
}
User user = User("Alice", 25, 0x123...);
映射详解:
// 定义映射
mapping(address => uint256) public balances;
// 设置值
balances[0x123...] = 100;
// 读取值
uint256 balance = balances[0x123...];
// 检查是否存在 (需要额外的映射)
mapping(address => bool) public hasBalance;
hasBalance[0x123...] = true;
变量作用域
contract Example {
// 状态变量 - 永久存储在区块链上
uint256 public globalVar;
string public name;
// 常量 - 编译时确定,不占用存储空间
uint256 constant MAX_SUPPLY = 1000000;
// 不可变变量 - 构造时设置,之后不可变
address immutable owner;
constructor() {
owner = msg.sender;
}
function example() public {
// 局部变量 - 函数执行完销毁
uint256 localVar = 10;
string memory tempStr = "hello";
// memory - 临时存储,函数执行完释放
string memory tempName = "Alice";
// storage - 永久存储,修改状态变量
globalVar = 20;
name = tempName;
// calldata - 只读,不能修改
function(string calldata) external {
// _input 是 calldata,只能读取
}
}
}
存储位置对比:
| 存储位置 | 持久性 | Gas 成本 | 可修改性 |
|---|---|---|---|
| storage | 永久 | 高 | 可读可写 |
| memory | 临时 | 低 | 可读可写 |
| calldata | 临时 | 最低 | 只读 |
函数可见性
contract Example {
// public - 任何人都可以调用
function publicFunc() public returns (uint256) {
return 1;
}
// external - 只能从外部调用,内部调用需要 this.externalFunc()
function externalFunc() external returns (uint256) {
return 2;
}
// internal - 只能在合约内部和继承合约中调用
function internalFunc() internal returns (uint256) {
return 3;
}
// private - 只能在合约内部调用
function privateFunc() private returns (uint256) {
return 4;
}
function test() public {
// 可以调用
uint256 a = publicFunc();
uint256 b = internalFunc();
uint256 c = privateFunc();
// 不能直接调用 external 函数
// uint256 d = externalFunc(); // 错误
// 需要通过 this 调用
uint256 d = this.externalFunc();
}
}
可见性对照表:
| 修饰符 | 合约内部 | 继承合约 | 外部调用 |
|---|---|---|---|
public |
✅ | ✅ | ✅ |
external |
❌ | ❌ | ✅ |
internal |
✅ | ✅ | ❌ |
private |
✅ | ❌ | ❌ |
函数修饰符
状态修改修饰符:
// view - 只读,不修改状态
function getBalance() public view returns (uint256) {
return balances[msg.sender];
}
// pure - 不读取也不修改状态
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
// payable - 可以接收 ETH
function deposit() public payable {
balances[msg.sender] += msg.value;
}
修饰符对比:
| 修饰符 | 读取状态 | 修改状态 | 接收 ETH | Gas 成本 |
|---|---|---|---|---|
view |
✅ | ❌ | ❌ | 低 |
pure |
❌ | ❌ | ❌ | 最低 |
payable |
✅ | ✅ | ✅ | 取决于操作 |
| (无) | ✅ | ✅ | ❌ | 取决于操作 |
何时使用 view/pure?
// ✅ 使用 view - 只读取状态变量
function getTotalSupply() public view returns (uint256) {
return totalSupply;
}
// ✅ 使用 pure - 纯计算,不涉及状态
function calculate(uint256 a, uint256 b) public pure returns (uint256) {
return a * b / 2;
}
// ❌ 不要使用 view - 修改了状态
function updateBalance() public view { // 错误!
balances[msg.sender] = 100;
}
// ❌ 不要使用 pure - 读取了状态变量
function getBalance() public pure returns (uint256) { // 错误!
return balances[msg.sender];
}
特殊变量
contract SpecialVariables {
// msg - 消息对象
function example() public payable {
address sender = msg.sender; // 调用者地址
uint256 value = msg.value; // 发送的 ETH 数量(wei)
bytes calldata data = msg.data; // 完整的调用数据
uint256 gas = msg.gas; // 剩余 gas
// block - 区块信息
uint256 blockNumber = block.number; // 当前区块号
uint256 timestamp = block.timestamp; // 当前区块时间戳
uint256 chainId = block.chainid; // 当前链 ID
address coinbase = block.coinbase; // 矿工地址
// tx - 交易信息
address origin = tx.origin; // 交易发起者地址
uint256 gasPrice = tx.gasprice; // Gas 价格
}
// 使用示例
function deposit() public payable {
require(msg.value > 0, "Must send ETH");
balances[msg.sender] += msg.value;
}
function getBlockInfo() public view returns (uint256, uint256) {
return (block.number, block.timestamp);
}
}
msg.sender vs tx.origin:
contract A {
function callB() public {
B(address(0x123...)).call();
}
}
contract B {
function call() public {
address sender = msg.sender; // 合约 A 的地址
address origin = tx.origin; // 用户的地址
}
}
⚠️ 安全警告: 不要使用 tx.origin 进行身份验证,容易被钓鱼攻击!
🛠️ 实操部分 (60分钟)
步骤 1: 使用 Remix IDE (10分钟)
什么是 Remix IDE?
Remix 是基于浏览器的 Solidity 开发环境,无需安装,开箱即用。
访问 Remix
-
- 打开 Remix
-
- 了解界面布局

不得不说,第一眼看Remix的主界面,内容非常多,也很凌乱。
-
- 创建工作区
-
• 找到正中间的Home页面
-
• 选择 "Create a new workspace"
-
• 选择 "Blank" 模板
-
• 点击 "Create"
步骤 2: 编写第一个合约 (20分钟)
创建合约文件
-
- 新建文件
-
• 在左侧文件管理器右键
-
• 选择 "New File"
-
• 命名为
HelloWeb3.sol
-
- 编写合约代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;/**
-
@title HelloWeb3
-
@dev 第一个智能合约示例
-
@author Your Name
*/
contract HelloWeb3 {
// ====== 状态变量 ======
string public message;
address public owner;
uint256 public changeCount;// ====== 事件 ======
event MessageChanged(
string oldMessage,
string newMessage,
address changedBy,
uint256 timestamp
);event DepositReceived(
address indexed from,
uint256 amount,
uint256 timestamp
);// ====== 构造函数 ======
constructor() {
owner = msg.sender;
message = "Hello Web3!";
changeCount = 0;emit MessageChanged( "", "Hello Web3!", msg.sender, block.timestamp );}
// ====== 修饰符 ======
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this");
_;
}// ====== 读函数 ======
/**
- @dev 获取完整信息
*/
function getInfo() public view returns (
string memory,
address,
uint256
) {
return (message, owner, changeCount);
}
/**
- @dev 获取合约余额
*/
function getBalance() public view returns (uint256) {
return address(this).balance;
}
// ====== 写函数 ======
/**
-
@dev 修改消息
-
@param _newMessage 新的消息
*/
function setMessage(string memory _newMessage) public onlyOwner {
string memory oldMessage = message;
message = _newMessage;
changeCount++;emit MessageChanged(
oldMessage,
_newMessage,
msg.sender,
block.timestamp
);
}
/**
- @dev 增加计数器
*/
function increment() public {
changeCount++;
}
// ====== 余额函数 ======
/**
- @dev 接收 ETH 的函数
*/
receive() external payable {
emit DepositReceived(msg.sender, msg.value, block.timestamp);
}
/**
- @dev 兜底函数
*/
fallback() external payable {}
/**
-
@dev 提取合约中的 ETH
*/
function withdraw() public onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No balance to withdraw");payable(owner).transfer(balance);
}
/**
- @dev 销毁合约
*/
function destroy() public onlyOwner {
selfdestruct(payable(owner));
}
}
- @dev 获取完整信息
代码解析
1. SPDX 许可标识
// SPDX-License-Identifier: MIT
-
• 告诉编译器代码使用的开源许可证
-
• MIT 是最宽松的许可证
-
• 必须放在第一行
2. Pragma 声明
pragma solidity ^0.8.20;
-
• 指定 Solidity 编译器版本
-
•
^0.8.20表示兼容 0.8.20 及以上,但不包括 0.9.0 -
•
>=0.8.20 <0.9.0是另一种写法
3. 注释
/// @dev 开发者注释
/// @param 参数说明
/// @return 返回值说明
/// @notice 用户可见的注释
4. 事件
event MessageChanged(...);
-
• 记录日志,不存储在状态中
-
• 降低 Gas 成本
-
• 便于前端监听
5. 修饰符
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this");
_; // 函数代码插入的位置
}
步骤 3: 编译合约 (5分钟)
编译步骤
-
- 打开编译器
-
• 点击左侧 "Solidity Compiler" 图标
-
• 或使用快捷键
Ctrl+S/Cmd+S
-
- 选择编译器版本
-
• 在 "Compiler" 下拉菜单中选择
0.8.20 -
• 确保与 Pragma 声明匹配
-
- 编译合约
-
• 点击 "Compile HelloWeb3.sol" 按钮
-
• 等待编译完成
-
- 检查结果
-
• ✅ 绿色勾号:编译成功
-
• ❌ 红色叉号:编译失败,查看错误信息

我们看到编译的时候生成几个json文件,可以点进去看看。
常见编译错误
| 错误 | 原因 | 解决方法 |
|---|---|---|
Expected identifier |
语法错误 | 检查代码语法 |
Type error |
类型不匹配 | 检查变量类型 |
Undeclared identifier |
变量未声明 | 检查变量名拼写 |
Function overload clash |
函数重载冲突 | 修改函数签名 |
步骤 4: 部署和测试 (25分钟)
部署合约
-
- 打开部署面板
- • 点击左侧 "Deploy & Run Transactions" 图标
-
- 选择环境
-
• 在 "Environment" 下拉菜单中选择 "Remix VM (Cancun)"
-
• 这是 Remix 内置的本地虚拟机
-
• 无需连接钱包,测试使用
-
- 部署合约
-
• 在 "Deploy" 按钮旁确认选中了
HelloWeb3合约 -
• 点击 "Deploy" 按钮
-
• 等待部署完成

- 查看部署结果
-
• 在下方 "Deployed Contracts" 下找到
HelloWeb3 -
• 点击展开合约,可以看到所有公开的函数和变量

测试合约
1. 查看初始值
# 点击 message 按钮
# 预期输出: "Hello Web3!"
# 点击 owner 按钮
# 预期输出: 你的部署地址
# 点击 changeCount 按钮
# 预期输出: 0
2. 测试 setMessage 函数
# 在 setMessage 输入框输入新消息
# 示例: "Welcome to Web3!"
# 点击按钮调用函数
# 查看控制台输出
# 应该看到事件日志
# 再次点击 message 按钮
# 预期输出: "Welcome to Web3!"

3. 测试访问控制
# 切换到另一个账户
# 点击 Remix VM 旁的账户下拉菜单
# 选择不同的账户
# 尝试调用 setMessage
# 预期: 交易失败,显示 "Only owner can call this"

4. 测试 ETH 接收(通过 receive 函数)
合约中定义了 receive() 函数,可以直接向合约地址转账:
# 步骤 1: 在 Deploy 面板找到 Value 输入框
# 输入金额: 5,选择单位: ether
# 步骤 2: 在 Deployed Contracts 中展开合约
# 找到最下方的合约地址按钮(低地址按钮)
# 步骤 3: 点击该地址按钮触发 receive() 函数
# 步骤 4: 点击 getBalance 按钮查看余额
# 预期输出: 5000000000000000000 (5 ETH in wei)
# 步骤 5: 查看控制台的事件日志
# 应该看到 DepositReceived 事件
# 步骤 6: 查看余额
# 应该看到少了5ETH

少了5ETH。

5. 测试提取 ETH
# 切换回 owner 账户
# 点击 withdraw 按钮
# 等待交易完成
# 点击 getBalance 按钮
# 预期输出: 0
# 查看当前账户余额
# 应该增加了 5 ETH

在94.99的基础上又增加了5ETH

6. 测试 getInfo 函数
# 点击 getInfo 按钮
# 预期输出: ("Welcome to Web3!", owner地址, 1)
查看事件日志
-
- 打开调试面板
- • 点击左侧 "Debug" 图标
-
- 查看事件
-
• 点击控制台中的交易
-
• 展开 "logs" 部分
-
• 查看事件参数,拷贝"transaction hash"中的内容到DEBUGGER中,就可以看到debug信息。

了解不同的部署环境
Remix VM
-
• 本地虚拟机
-
• 快速,免费
-
• 适合开发测试
Injected Provider - MetaMask
-
• 连接真实的 MetaMask 钱包
-
• 可以部署到测试网
-
• 需要支付 Gas
Web3 Provider
-
• 连接到外部节点
-
• 需要提供 RPC URL
今天我人只测试和体验了Remix VM,后续有时间我们再体验Injected Provider和Web3 Provider。
✅ 今日产出检查清单
理论知识
-
• 理解智能合约的概念和特点
-
• 了解 EVM 的工作原理
-
• 掌握 Solidity 语言的特点
-
• 理解数据类型(值类型、引用类型)
-
• 掌握函数的可见性修饰符
-
• 理解存储位置(storage、memory、calldata)
实操能力
-
• 熟悉 Remix IDE 的界面和功能
-
• 成功创建和编译智能合约
-
• 在 Remix VM 中部署合约
-
• 测试合约的各种功能
-
• 理解事件的用途
-
• 掌握访问控制的使用
代码能力
-
• 能够定义状态变量
-
• 能够编写构造函数
-
• 能够创建和使用修饰符
-
• 能够定义和触发事件
-
• 能够处理 ETH 接收和发送
-
• 能够实现访问控制
💡 常见问题
Q1: 什么是 wei?
A: wei 是以太币的最小单位。1 ETH = 10^18 wei。这是为了避免浮点数精度问题。
1 ETH = 1,000,000,000,000,000,000 wei
1 Gwei = 1,000,000,000 wei (10^9)
1 Ether = 10^18 wei
Q2: view 和 pure 函数不需要 Gas 吗?
A: 不完全正确。如果外部调用 view/pure 函数,不需要支付 Gas。但如果在合约内部调用,仍然需要 Gas。
Q3: 为什么需要 payable 修饰符?
A: 只有带有 payable 修饰符的函数才能接收 ETH。这是为了防止意外转账和潜在的安全问题。
Q4: 事件和状态变量有什么区别?
A:
-
• 事件: 记录日志,不存储在状态中,成本低,用于前端监听
-
• 状态变量: 永久存储,成本高,可以随时读取
Q5: 如何选择存储位置?
A:
-
• storage: 需要永久保存的数据
-
• memory: 函数内部临时使用的数据
-
• calldata: 只读的输入参数
📖 课后作业(选做)
-
- 扩展合约功能
-
• 添加一个计数器,记录合约被调用的总次数
-
• 添加一个函数,可以设置最大修改次数
-
• 实现一个简单的投票机制
-
- 测试不同数据类型
-
• 创建一个使用 mapping 的函数
-
• 测试数组的添加和删除
-
• 尝试使用 struct
-
- 优化 Gas 成本
-
• 比较 uint256 和 uint8 的 Gas 差异
-
• 测试使用 calldata vs memory 的区别
-
• 使用事件替代部分状态变量
-
- 记录学习笔记
-
• 记录今天学到的所有语法
-
• 记录遇到的错误和解决方法
-
• 总结最佳实践
🎯 明天预告
明天我们将深入学习智能合约的进阶功能:
-
• 学习继承和多重继承
-
• 掌握错误处理(require、revert、assert)
-
• 理解 ERC-20 代币标准
-
• 实现一个完整的 ERC-20 代币合约
-
• 学习智能合约的安全最佳实践
预习建议:
-
• 阅读关于面向对象编程的基础知识
-
• 了解代币的概念和经济模型
-
• 研究一些知名的 ERC-20 代币
学习时长:约 100 分钟
难度:⭐⭐☆☆☆
下次学习:Day 3 - 智能合约进阶与 ERC-20 代币
详细参见: 春节7天Web3学习计划:我决定用假期给自己"充值"
祝你学习顺利! 🚀