Module 03: Smart Contracts & dApp Development
Solidity 开发、Hardhat 工具链、ERC 标准与前端集成
上一模块思考题参考
Q: 共识机制保证所有节点对交易顺序达成一致——这对智能合约的执行意味着什么? 交易顺序决定了合约状态的最终结果。共识保证所有节点按同一顺序执行同一组交易,因此每个节点计算出的状态一致。如果顺序不同,同一个转账操作可能在一个节点上成功、在另一个节点上因余额不足而失败。
Q: 当一个智能合约在执行过程中调用了另一个合约(外部调用),共识机制需要保证什么?这种合约间的调用会带来什么新的复杂性? 共识需要保证整条调用链的执行结果在所有节点上完全一致,包括 gas 消耗和状态变更。外部调用带来的复杂性包括:被调用合约可能回调原合约(重入)、调用链中任何一步失败可能导致整体回滚、gas 在调用链中逐层传递和消耗。
智能合约是什么
智能合约是部署在区块链上的程序。一旦部署,代码不可篡改,执行结果由网络共识保证。它解决的核心问题是:在没有可信第三方的情况下,让代码自动执行协议。
传统合同依赖法律体系和中间人来执行。智能合约把执行逻辑写成代码,由 EVM(以太坊虚拟机)运行,结果公开可验证。这使得去中心化金融(DeFi)、NFT、DAO 等应用成为可能。
Solidity 基础
Solidity 是以太坊智能合约的主要编程语言,语法接近 JavaScript/C++,但有几个关键区别:所有状态变量存储在区块链上,函数调用可能消耗真金白银(gas),部署后代码不可修改。
核心概念
数据类型:uint256(无符号整数)、address(20 字节地址)、bool、string、mapping(键值映射)、struct(自定义结构体)。
函数可见性:
public— 内外部均可调用external— 仅外部调用(更省 gas)internal— 仅合约内部和子合约private— 仅当前合约
修饰符(modifier):在函数执行前插入检查逻辑,常用于权限控制。
事件(event):写入交易日志,链下应用(如前端)可以监听。不占用合约存储,成本低。
示例:简单存储合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleStorage {
uint256 private _value;
address public owner;
event ValueChanged(uint256 newValue, address changedBy);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor() {
owner = msg.sender;
}
function set(uint256 newValue) external onlyOwner {
_value = newValue;
emit ValueChanged(newValue, msg.sender);
}
function get() external view returns (uint256) {
return _value;
}
}
这个合约展示了状态变量、构造函数、modifier、event 的基本用法。view 关键字表示函数不修改状态,调用不消耗 gas。
EVM 与 Gas
EVM 执行模型
Solidity 代码编译成 字节码(bytecode),由 EVM 逐条执行。EVM 是一个栈式虚拟机,所有以太坊节点运行相同的 EVM,确保执行结果一致。
合约部署时,字节码永久存储在链上。每次调用合约函数,EVM 从字节码中读取操作码(opcode)并执行。
Gas 机制
每条 EVM 操作码有固定的 gas 消耗。例如:加法 3 gas,存储写入 20000 gas(新 slot)或 5000 gas(更新已有 slot)。
为什么需要 gas? 防止无限循环和资源滥用。没有 gas 机制,任何人都可以部署一个死循环合约,瘫痪整个网络。
Gas 费用 = gas 消耗量 × gas 价格。gas 价格由市场供需决定,网络拥堵时价格上升。
Gas 优化要点
- 存储(SSTORE)是最贵的操作,尽量减少链上存储
- 用
calldata代替memory读取外部函数的数组参数 - 将多个小变量打包进同一个 256-bit 存储槽(storage slot packing)
- 用
error代替require的字符串消息(Solidity 0.8.4+) - 循环中避免重复读取状态变量,先缓存到本地变量
开发工具链
Hardhat
Hardhat 是目前最主流的以太坊开发框架,提供编译、测试、部署、调试的完整工具链。
典型项目结构:
my-project/
├── contracts/ # Solidity 合约
│ └── SimpleStorage.sol
├── test/ # 测试文件
│ └── SimpleStorage.test.js
├── scripts/ # 部署脚本
│ └── deploy.js
├── hardhat.config.js # 配置文件
└── package.json
核心命令:
npx hardhat compile— 编译合约npx hardhat test— 运行测试npx hardhat node— 启动本地节点npx hardhat run scripts/deploy.js --network sepolia— 部署到测试网
Foundry
Foundry 是 Rust 编写的替代工具链,特点是快和用 Solidity 写测试(不需要 JavaScript)。核心工具:forge(编译测试)、cast(链上交互)、anvil(本地节点)。
选择建议:前端开发者习惯 JS 生态选 Hardhat;追求速度和纯 Solidity 测试选 Foundry。两者可以混用。
合约交互
前端或脚本与合约交互,最常用的库是 ethers.js(v6)。三个核心概念:
- Provider — 连接区块链节点,只读操作
- Signer — 代表一个账户,可以签名交易
- Contract — 合约实例,绑定 ABI 和地址
示例:读写合约
import { ethers } from "ethers";
// 连接 MetaMask
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// 合约 ABI(简化版,只保留需要的函数)
const abi = [
"function get() view returns (uint256)",
"function set(uint256 newValue)",
"event ValueChanged(uint256 newValue, address changedBy)"
];
const contract = new ethers.Contract(
"0x1234...合约地址",
abi,
signer
);
// 读取(不消耗 gas)
const value = await contract.get();
console.log("当前值:", value.toString());
// 写入(发送交易,消耗 gas)
const tx = await contract.set(42);
await tx.wait(); // 等待交易被挖矿确认
// 监听事件
contract.on("ValueChanged", (newValue, changedBy) => {
console.log(`值变更为 ${newValue},操作者 ${changedBy}`);
});
ethers.js v6 的人类可读 ABI 格式省去了手动编写完整 ABI JSON 的麻烦。读操作(view/pure 函数)直接返回结果;写操作返回交易对象,需要 wait() 等待确认。
测试与部署
编写测试
用 Hardhat + Chai 编写测试:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("SimpleStorage", function () {
it("should store and retrieve a value", async function () {
const Storage = await ethers.getContractFactory("SimpleStorage");
const storage = await Storage.deploy();
await storage.set(42);
expect(await storage.get()).to.equal(42n);
});
it("should reject non-owner", async function () {
const [owner, other] = await ethers.getSigners();
const Storage = await ethers.getContractFactory("SimpleStorage");
const storage = await Storage.deploy();
await expect(
storage.connect(other).set(99)
).to.be.revertedWith("Not owner");
});
});
测试在 Hardhat 内置的本地链上运行,速度快且免费。
部署到测试网
- 在
hardhat.config.js配置 Sepolia 网络(RPC URL + 私钥) - 从水龙头(faucet)获取测试 ETH
- 运行
npx hardhat run scripts/deploy.js --network sepolia - 部署后在 Etherscan 上验证合约源码:
npx hardhat verify --network sepolia <合约地址>
验证后,任何人都可以在 Etherscan 上直接读取和调用合约函数。
常见合约模式
ERC-20:同质化代币
定义了 transfer、approve、transferFrom 等标准接口。所有 DeFi 协议依赖这个标准实现代币互操作。OpenZeppelin 提供了经过审计的实现,几行代码即可创建代币。
ERC-721:非同质化代币(NFT)
每个 token 有唯一 ID,不可互换。核心接口:ownerOf、transferFrom、tokenURI(指向元数据)。数字艺术、游戏道具、域名等场景。
代理模式(Proxy Pattern)
合约部署后代码不可修改,但业务需求会变。代理模式的解决方案:用户调用代理合约(Proxy),代理通过 delegatecall 将逻辑转发给实现合约(Implementation)。升级时只需将代理指向新的实现合约,存储数据保留。
常见实现:透明代理(Transparent Proxy)、UUPS。OpenZeppelin 提供了标准化的代理库。
访问控制(Access Control)
比简单的 onlyOwner 更灵活的权限管理。OpenZeppelin 的 AccessControl 合约支持基于角色的权限:定义多个角色(admin、minter、pauser),按角色授权函数调用。
推荐资源
| 资源 | 类型 | 适合阶段 | 说明 |
|---|---|---|---|
| CryptoZombies | 交互教程 | 入门 | 通过做游戏学 Solidity,零基础友好 |
| Solidity 官方文档 | 文档 | 全阶段 | 语法细节和最新特性的权威参考 |
| Patrick Collins 课程 | 视频 | 入门→进阶 | 32 小时免费课程,覆盖 Hardhat 和 Foundry |
| OpenZeppelin Contracts | 代码库 | 进阶 | 经过审计的标准合约实现,生产环境直接使用 |
| Ethereum Book | 书籍 | 进阶 | 深入理解以太坊底层原理 |
思考题
- ERC-20 标准定义了
approve+transferFrom的授权模式——DEX 如何利用这个机制实现代币交换?如果没有统一的代币标准,去中心化交易所能否存在? - Gas 机制对 DeFi 协议的可组合性有什么影响?当一笔交易需要调用 5 个不同协议的合约时,gas 成本如何累加,用户体验会怎样?
- 代理模式允许合约升级——这对 DeFi 协议的”去信任”承诺意味着什么?用户如何判断一个可升级合约是否安全?
下一步
掌握智能合约开发基础后,Module 04 将进入区块链安全与审计——重入攻击、溢出漏洞、闪电贷攻击等实际安全问题,以及如何编写安全的合约代码。