← 返回预习计划
Module 03

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 字节地址)、boolstringmapping(键值映射)、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 内置的本地链上运行,速度快且免费。

部署到测试网

  1. hardhat.config.js 配置 Sepolia 网络(RPC URL + 私钥)
  2. 从水龙头(faucet)获取测试 ETH
  3. 运行 npx hardhat run scripts/deploy.js --network sepolia
  4. 部署后在 Etherscan 上验证合约源码:npx hardhat verify --network sepolia <合约地址>

验证后,任何人都可以在 Etherscan 上直接读取和调用合约函数。


常见合约模式

ERC-20:同质化代币

定义了 transferapprovetransferFrom 等标准接口。所有 DeFi 协议依赖这个标准实现代币互操作。OpenZeppelin 提供了经过审计的实现,几行代码即可创建代币。

ERC-721:非同质化代币(NFT)

每个 token 有唯一 ID,不可互换。核心接口:ownerOftransferFromtokenURI(指向元数据)。数字艺术、游戏道具、域名等场景。

代理模式(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书籍进阶深入理解以太坊底层原理

思考题

  1. ERC-20 标准定义了 approve + transferFrom 的授权模式——DEX 如何利用这个机制实现代币交换?如果没有统一的代币标准,去中心化交易所能否存在?
  2. Gas 机制对 DeFi 协议的可组合性有什么影响?当一笔交易需要调用 5 个不同协议的合约时,gas 成本如何累加,用户体验会怎样?
  3. 代理模式允许合约升级——这对 DeFi 协议的”去信任”承诺意味着什么?用户如何判断一个可升级合约是否安全?

下一步

掌握智能合约开发基础后,Module 04 将进入区块链安全与审计——重入攻击、溢出漏洞、闪电贷攻击等实际安全问题,以及如何编写安全的合约代码。

动手练习