一.智能合约基础
智能合约是运行在区块链(以太坊)上的程序,部署后代码不可修改,调用即交易,需要支付 Gas。Solidity 是编写以太坊智能合约的主流语言,语法类似 JavaScript + C++,编译后在 EVM(以太坊虚拟机) 上执行。
开发环境推荐 Remix IDE(浏览器直接用,无需安装):编写 -> 编译 -> 部署 -> 调用。
// SPDX-License-Identifier: MIT // 开源协议声明
pragma solidity ^0.8.0; // 编译器版本
contract HelloWorld {
string public message; // public 状态变量,自动生成 getter
// 构造函数:部署时执行一次
constructor() {
message = "Hello, Solidity!";
}
// 修改状态:会发起交易,消耗 Gas
function updateMessage(string memory newMessage) public {
message = newMessage;
}
// 只读:不消耗 Gas(外部调用时)
function getMessage() public view returns (string memory) {
return message;
}
}
Gas 简单理解:每个操作都有固定成本,写存储最贵,读最便宜;Gas 不足交易回滚。
二.EVM存储结构
数据的三种存储位置:
| 位置 | 特点 | Gas |
|---|---|---|
| storage | 永久存储在链上,状态变量默认 | 最贵 |
| memory | 函数执行期间的临时内存,可修改 | 中 |
| calldata | 外部调用传入的只读参数 | 最省 |
contract Storage {
uint[] public nums; // storage
// calldata:只读,外部函数参数首选
function sum(uint[] calldata arr) external pure returns (uint total) {
for (uint i = 0; i < arr.length; i++) {
total += arr[i];
}
}
// memory:可修改的临时数组
function createArr() public pure returns (uint[] memory) {
uint[] memory tmp = new uint[](3);
tmp[0] = 1;
return tmp;
}
}
选择原则:外部函数的数组/字符串参数用 calldata;需要修改的临时数据用 memory;需要永久保存的才用 storage。
三.数据类型基础
值类型:bool、int/uint、address、bytes、enum。引用类型:数组、struct、mapping。
1.布尔和整数
bool public flag = true;
uint256 public a = 10; // 无符号,uint 即 uint256
int256 public b = -5; // 有符号
uint256 public c = 10 / 3; // 3,整数除法没有小数
0.8.0 之后整数溢出会自动 revert,需要跳过检查时用 unchecked。
uint8 x = 255;
// x + 1; // 报错回滚
unchecked { x = x + 1; } // 0,不检查溢出
2.地址类型
address public owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
address payable public wallet; // 可以接收 ETH
uint256 public bal = owner.balance; // 查询余额
3.字节和字符串
bytes32 public b32 = "abc"; // 定长字节,省 Gas
bytes public bs = "abc"; // 动态字节
string public str = "Hello"; // 字符串,不能直接比较、不能直接取长度
// 比较字符串
function eq(string memory x, string memory y) public pure returns (bool) {
return keccak256(bytes(x)) == keccak256(bytes(y));
}
4.枚举和类型转换
enum Status { Pending, Done } // 底层是 uint8,从 0 开始
Status public s = Status.Pending;
uint8 small = 100;
uint256 big = small; // 隐式转换:小 -> 大
uint8 s2 = uint8(big); // 显式转换:大 -> 小可能截断
address addr = address(uint160(123));
5.运算符:算术 + - * / % **、比较 == != > < >= <=、逻辑 && || !(短路求值)、位运算 & | ^ << >>。
四.数组
uint[3] public fixedArr = [1, 2, 3]; // 定长数组,长度固定
uint[] public dynArr; // 动态数组,长度可变
基本操作:
uint[] public arr;
function add(uint v) public { arr.push(v); }
function remove() public { arr.pop(); } // 删除最后一个
function len() public view returns (uint) { return arr.length; }
function get(uint i) public view returns (uint) { return arr[i]; }
// delete 只是把值重置为 0,长度不变
function clear(uint i) public { delete arr[i]; }
删除中间元素:
// 方式一:保持顺序(循环移动,Gas 高)
function removeOrdered(uint i) public {
for (uint j = i; j < arr.length - 1; j++) {
arr[j] = arr[j + 1];
}
arr.pop();
}
// 方式二:不保序(用最后一个覆盖,Gas 低,推荐)
function removeFast(uint i) public {
arr[i] = arr[arr.length - 1];
arr.pop();
}
二维数组:uint[3][4] 表示 4 个长度为 3 的数组。memory 数组必须定长创建,不能 push:
uint[] memory tmp = new uint[](5);
Gas 注意:不要遍历无限增长的数组;缓存 length;限制数组最大长度;大批量分批处理。
五.映射和结构体
1.mapping
// mapping(键类型 => 值类型)
mapping(address => uint) public balances;
function set() public {
balances[msg.sender] = 100;
}
特点:不能遍历、没有 length、不存在的 key 返回默认值 0、不保存 key。需要遍历时配合数组使用。
2.嵌套 mapping(ERC20 授权)
// 所有者 => 被授权人 => 额度
mapping(address => mapping(address => uint)) public allowance;
allowance[msg.sender][spender] = 100;
3.struct
struct User {
string name;
uint age;
bool exists; // 判断用户是否存在
}
mapping(address => User) public users;
function register(string memory name, uint age) public {
require(!users[msg.sender].exists, "exists");
users[msg.sender] = User(name, age, true);
}
function grow() public {
User storage u = users[msg.sender]; // storage 引用,直接修改链上数据
u.age += 1;
}
4.mapping + array 组合(可遍历)
mapping(address => bool) public isUser;
address[] public userList;
function add() public {
require(!isUser[msg.sender], "exists");
isUser[msg.sender] = true;
userList.push(msg.sender);
}
常见模式:用户管理、ID 自增、双向映射、一对多关系、计数器。
六.函数与修饰符
function 函数名(参数) 可见性 状态修饰符 returns (返回值) {
函数体
}
1.可见性
| 关键字 | 说明 |
|---|---|
| public | 内外部都可调用 |
| external | 只能外部调用(参数用 calldata 更省 Gas) |
| internal | 当前合约和子合约可调用 |
| private | 仅当前合约可调用(链上数据依然可被读取,不是隐私) |
2.状态修饰符
uint public x;
function read() public view returns (uint) { return x; } // view:只读状态
function add(uint a, uint b) public pure returns (uint) { // pure:不读不写状态
return a + b;
}
function deposit() public payable {} // payable:可接收 ETH
3.自定义 modifier
address public owner;
constructor() { owner = msg.sender; }
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_; // 被修饰函数的代码在这里执行
}
function setValue(uint v) public onlyOwner {}
// 多个 modifier 从左到右依次执行
4.函数重载(同名不同参数)
function show(uint a) public pure returns (uint) { return a; }
function show(string memory s) public pure returns (string memory) { return s; }
5.receive 和 fallback
receive() external payable {} // 收到纯 ETH 转账时触发
fallback() external payable {} // 调用不存在的函数时触发
七.控制流语句
1.if else 和三元运算符
function grade(uint score) public pure returns (string memory) {
if (score >= 90) {
return "A";
} else if (score >= 60) {
return "B";
}
return "C";
}
uint max = a > b ? a : b;
2.循环(注意 Gas)
// for
for (uint i = 0; i < 10; i++) {
if (i == 3) continue; // 跳过
if (i == 8) break; // 跳出
}
// while
uint n = 0;
while (n < 10) { n++; }
// do-while 至少执行一次
do { n--; } while (n > 0);
链上循环每一步都消耗 Gas,循环次数不能无限增长,否则交易会因 Gas 超限失败(DoS)。
3.require / assert / revert
function withdraw(uint amount) public {
require(amount > 0, "Zero amount"); // 校验输入,失败回退并返还剩余 Gas
if (balance < amount) revert("No balance"); // 灵活抛错
assert(balance >= 0); // 校验内部不变量,失败说明有 Bug
}
4.状态机模式
enum State { Active, Success, Failed }
State public state = State.Active;
modifier inState(State s) {
require(state == s, "Wrong state");
_;
}
八.特殊类型与全局变量
1.address 与三种转账方式
address payable to = payable(msg.sender);
to.transfer(1 ether); // 失败自动回滚,限 2300 Gas
bool ok = to.send(1 ether); // 失败返回 false,需要手动检查
(bool success, ) = to.call{value: 1 ether}(""); // 最灵活,推荐
require(success, "Failed");
推荐使用 call,并配合 require 检查返回值。
2.全局变量
msg.sender // 调用者地址
msg.value // 随调用发送的 ETH(wei)
block.timestamp // 当前区块时间戳(秒)
block.number // 当前区块号
tx.origin // 交易发起者(⚠️ 不要用于权限判断,会被钓鱼攻击)
单位:1 ether = 1e18 wei,时间 1 days = 86400 秒。
3.enum 枚举(见第三节):用有名字的状态代替魔法数字,uint(status) 可转成整数。
4.constant 和 immutable
uint public constant MAX = 1000; // 编译时确定,写死在代码里,最省 Gas
address public immutable owner; // 部署时(构造函数)确定,之后不可改
constructor() {
owner = msg.sender;
}
九.简单代币合约 ERC20
ERC20 是可互换代币的标准接口:
name / symbol / decimals / totalSupply // 基本信息
balanceOf(owner) // 查询余额
transfer(to, amount) // 转账
approve(spender, amount) // 授权
allowance(owner, spender) // 查询授权额度
transferFrom(from, to, amount) // 代为转账
事件:Transfer、Approval
最简实现:
contract MyToken {
string public name = "MyToken";
string public symbol = "MT";
uint8 public decimals = 18;
uint256 public totalSupply;
address public owner;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(uint256 supply) {
owner = msg.sender;
totalSupply = supply * 10**decimals; // 乘以 10 的 decimals 次方
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address to, uint256 amount) public returns (bool) {
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(balanceOf[from] >= amount, "Insufficient balance");
require(allowance[from][msg.sender] >= amount, "Insufficient allowance");
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
// 铸造:只有 owner 可以
function mint(address to, uint256 amount) public {
require(msg.sender == owner, "Only owner");
totalSupply += amount;
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}
// 销毁:销毁自己的代币
function burn(uint256 amount) public {
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
totalSupply -= amount;
balanceOf[msg.sender] -= amount;
emit Transfer(msg.sender, address(0), amount);
}
}
要点:mint 的 from 是零地址,burn 的 to 是零地址;approve 会覆盖旧额度;transferFrom 要扣减授权额度。生产环境直接使用 OpenZeppelin 的 ERC20。
十.合约继承
contract Parent {
address public owner;
constructor() { owner = msg.sender; }
function hello() public virtual returns (string memory) { // virtual:允许被重写
return "parent";
}
}
contract Child is Parent { // is 表示继承
function hello() public override returns (string memory) { // override:重写
return "child";
}
}
1.多重继承:contract C is A, B,顺序从最基础到最派生,按 C3 线性化确定调用顺序。
2.super:调用父合约的函数
function hello() public override returns (string memory) {
return super.hello();
}
3.构造函数传参
contract Parent {
string public name;
constructor(string memory _name) { name = _name; }
}
contract Child is Parent("ww") {} // 方式一:固定参数
contract Child2 is Parent {
constructor() Parent("zz") {} // 方式二:构造时传参
}
4.抽象合约和接口
// 抽象合约:有未实现的函数,不能直接部署
abstract contract Animal {
function speak() public virtual returns (string memory);
}
// 接口:只有函数声明,全部 external
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
}
可见性提示:private 子合约不可访问,internal 子合约可访问。
十一.库合约 Library
库是可复用的无状态代码,不能有状态变量、不能接收 ETH、不能继承。
library MathLib {
function add(uint a, uint b) internal pure returns (uint) {
return a + b;
}
}
contract Test {
using MathLib for uint; // 给 uint 类型绑定库函数
function calc(uint x, uint y) public pure returns (uint) {
return x.add(y); // 等价于 MathLib.add(x, y)
}
}
- 内部库(internal):代码被编译进调用合约,最常用。
- 外部库(public/external):单独部署,通过
delegatecall调用,使用前需要链接库地址。
OpenZeppelin 是最常用的开源库:Ownable、ERC20、ERC721、ReentrancyGuard、Pausable、AccessControl 等。
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MT") {
_mint(msg.sender, 1000 * 10**decimals());
}
}
十二.事件 Events
事件把数据写入交易日志,比存储便宜得多,合约内部读不到,主要给前端/链下服务监听。
// indexed 最多 3 个,可用来按条件过滤查询
event Transfer(address indexed from, address indexed to, uint value);
function send(address to, uint v) public {
emit Transfer(msg.sender, to, v); // 触发事件
}
indexed参数放入 topics,方便过滤;不加 indexed 的放在 data 里。- 匿名事件
event Log(uint a) anonymous;:不记录事件签名,最多 4 个 indexed。 - 不要在事件里写敏感信息,日志是公开的。
- 常用于:转账记录、NFT 交易、投票、多签操作。
十三.错误处理和自定义错误
1.三种基础机制
| 关键字 | 用途 | 失败时 |
|---|---|---|
| require | 校验输入和外部条件 | 回退,返还剩余 Gas |
| assert | 校验内部不变量 | 回退(Panic 错误) |
| revert | 灵活抛错 | 回退 |
2.自定义错误(0.8.4+),比字符串更省 Gas,还能带参数
error InsufficientBalance(uint available, uint required);
error Unauthorized();
function withdraw(uint amount) public {
if (msg.sender != owner) revert Unauthorized();
if (balance < amount) revert InsufficientBalance(balance, amount);
balance -= amount;
}
3.try-catch:只能捕获外部调用和合约创建的失败
function safeCall(address target) public returns (bool) {
try ITarget(target).doSomething() returns (uint v) {
return v > 0;
} catch Error(string memory reason) { // require/revert 带字符串
return false;
} catch (bytes memory) { // 其他所有错误
return false;
}
}
十四.合约间调用
1.接口调用(最常用、最安全)
interface ICounter {
function count() external view returns (uint);
function inc() external;
}
contract Caller {
function callInc(address counter) public {
ICounter(counter).inc();
}
}
2.底层调用 call / delegatecall / staticcall
// call:在目标合约上下文中执行,修改目标合约的存储
(bool ok, bytes memory data) = target.call(abi.encodeWithSignature("setValue(uint256)", 1));
// delegatecall:借用目标合约的代码,在“自己”的存储里执行(代理合约的基础)
(bool ok2, ) = target.delegatecall(abi.encodeWithSignature("setValue(uint256)", 1));
// staticcall:只读调用,不允许修改状态
(bool ok3, bytes memory ret) = target.staticcall(abi.encodeWithSignature("getValue()"));
3.创建合约
Child c = new Child(); // new:地址由部署者和 nonce 决定
Child c2 = new Child{salt: bytes32(uint(1))}(); // create2:地址可预测
4.外部调用要防重入(见下一节),并检查返回值。
十五.智能合约安全基础
1.重入攻击:先转账后改状态,被攻击合约在 receive 里反复调用 withdraw
// ❌ 不安全
function withdraw() public {
uint bal = balances[msg.sender];
(bool ok, ) = msg.sender.call{value: bal}(""); // 先转账
require(ok);
balances[msg.sender] = 0; // 后清零(太晚了)
}
// ✅ CEI:Checks -> Effects -> Interactions
function withdraw() public {
uint bal = balances[msg.sender];
require(bal > 0, "No balance"); // Checks
balances[msg.sender] = 0; // Effects
(bool ok, ) = msg.sender.call{value: bal}(""); // Interactions
require(ok, "Failed");
}
也可以使用 OpenZeppelin 的 ReentrancyGuard:
contract Vault is ReentrancyGuard {
function withdraw() public nonReentrant {}
}
2.整数溢出:0.8.0 之后默认检查,用 unchecked 时要自己保证安全。
3.权限控制:敏感函数必须加 onlyOwner 等限制,不要用 tx.origin 验权。
4.DoS 攻击:不要循环给大量用户转账;使用拉取模式,让用户自己提款。
5.前端运行(抢跑):用 Commit-Reveal(先提交哈希,再揭示)防御。
十六.Gas优化技巧
1.存储槽打包:一个槽 32 字节,小类型相邻放一起
// ❌ 占 3 个槽
uint128 a; uint256 b; uint128 c;
// ✅ 占 2 个槽
uint128 a; uint128 c; uint256 b;
2.局部存储指针,避免重复读取 storage
User storage u = users[msg.sender];
u.age += 1;
u.score += 1;
3.常用小技巧:
// external + calldata
function f(uint[] calldata arr) external {}
// 缓存数组长度
uint len = arr.length;
for (uint i = 0; i < len; ) {
unchecked { i++; } // 确定不会溢出时才使用
}
// 能用 constant / immutable 就用
uint public constant MAX = 100;
// 自定义错误代替字符串
error NotOwner();
// 批量操作,减少交易次数
function batchTransfer(address[] calldata tos, uint amount) external {}
// 只给前端看的数据用 event,不存 storage
emit Log(msg.sender, amount);
工具:Hardhat Gas Reporter、forge snapshot、forge test --gas-report。
十七.智能合约设计模式
1.访问控制:Ownable(单管理员)/ RBAC(多角色)
mapping(address => bool) public admins;
modifier onlyAdmin() {
require(admins[msg.sender], "Not admin");
_;
}
2.提现模式(Pull over Push):让用户自己来取钱
mapping(address => uint) public pending;
function withdraw() public {
uint amount = pending[msg.sender];
pending[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
}
3.状态机模式:见第七节。
4.代理模式:用户调用代理合约,代理用 delegatecall 转发给逻辑合约,实现可升级;注意存储布局要兼容。
contract Proxy {
address public implementation; // 逻辑合约地址,可更换
fallback() external payable {
(bool ok, ) = implementation.delegatecall(msg.data);
require(ok);
}
}
5.工厂模式:用一个合约批量创建其他合约
contract TokenFactory {
address[] public tokens;
function create() public {
Child c = new Child();
tokens.push(address(c));
}
}
6.紧急停止模式(Pausable)
bool public paused;
modifier whenNotPaused() {
require(!paused, "Paused");
_;
}
function pause() public onlyOwner { paused = true; }
十八.NFT市场
NFT 使用 ERC721 标准,每个 tokenId 唯一(ERC20 是可互换的)。
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract MyNFT is ERC721 {
uint public nextId;
constructor() ERC721("MyNFT", "MNFT") {}
function mint() public {
_safeMint(msg.sender, nextId++);
}
}
核心函数:ownerOf(id)、balanceOf(owner)、approve、setApprovalForAll、transferFrom、tokenURI(id)(元数据地址)。
NFT 市场最简逻辑:上架(挂单)-> 购买 -> 下架,市场合约需要用户先授权。
struct Listing {
address seller;
uint price;
}
mapping(address => mapping(uint => Listing)) public listings; // NFT合约 => tokenId => 挂单
function list(address nft, uint id, uint price) public {
require(IERC721(nft).ownerOf(id) == msg.sender, "Not owner");
listings[nft][id] = Listing(msg.sender, price);
}
function buy(address nft, uint id) public payable {
Listing memory l = listings[nft][id];
require(msg.value == l.price, "Wrong price");
delete listings[nft][id]; // 先改状态
IERC721(nft).transferFrom(l.seller, msg.sender, id); // 转 NFT
(bool ok, ) = l.seller.call{value: msg.value}(""); // 付款给卖家
require(ok);
}
扩展:版税(ERC2981)、拍卖、平台手续费。
十九.Hardhat 3 环境搭建与合约部署
Hardhat 是 JavaScript/TypeScript 生态的合约开发框架:编译、测试、部署一体。
// 常用命令
npx hardhat compile // 编译
npx hardhat test // 测试
npx hardhat node // 启动本地链
npx hardhat clean // 清理缓存
目录结构:
project
|- contracts 合约
|- test 测试
|- ignition
|- modules 部署模块
|- hardhat.config.ts 配置(编译器版本、网络)
|- .env 私钥、RPC(不要提交)
Ignition 部署模块:
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
export default buildModule("CounterModule", (m) => {
const counter = m.contract("Counter");
return { counter };
});
// 部署到本地 / 测试网
npx hardhat ignition deploy ignition/modules/Counter.ts
npx hardhat ignition deploy ignition/modules/Counter.ts --network sepolia
二十.Hardhat 3 单元测试
测试使用 Mocha(describe / it 组织)+ Chai(expect 断言)。
import { expect } from "chai";
import { network } from "hardhat";
const { ethers, networkHelpers } = await network.connect();
// Fixture:部署一次,每个用例回到同一快照
async function deployFixture() {
const [owner, addr1] = await ethers.getSigners();
const counter = await ethers.deployContract("Counter");
return { counter, owner, addr1 };
}
describe("Counter", function () {
it("初始值为 0", async function () {
const { counter } = await networkHelpers.loadFixture(deployFixture);
expect(await counter.x()).to.equal(0n);
});
it("inc 后加 1", async function () {
const { counter } = await networkHelpers.loadFixture(deployFixture);
await counter.inc();
expect(await counter.x()).to.equal(1n);
});
});
常见测试点:
// 事件
await expect(token.transfer(addr1, 10n)).to.emit(token, "Transfer");
// 回滚
await expect(token.transfer(addr1, 999999n)).to.be.revertedWith("Insufficient balance");
await expect(token.withdraw(1n)).to.be.revertedWithCustomError(token, "InsufficientBalance");
// 切换调用者
await token.connect(addr1).transfer(owner, 1n);
还包括:时间旅行(快进区块时间)、快照恢复、覆盖率、Gas 报告。
二十一.Foundry
Foundry 用 Rust 编写,测试直接用 Solidity 写,速度快。工具链:
| 工具 | 作用 |
|---|---|
| forge | 编译、测试、部署脚本 |
| cast | 和链交互(查询、发交易) |
| anvil | 本地测试链 |
| chisel | Solidity 交互式 REPL |
forge init demo // 初始化项目
forge build // 编译
forge test // 测试
forge test -vvv // 显示详细日志
forge script script/Counter.s.sol --rpc-url <url> --broadcast // 部署
forge install openzeppelin/openzeppelin-contracts // 安装依赖
anvil // 启动本地链
测试合约(以 test 开头的函数是用例):
import {Test} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";
contract CounterTest is Test {
Counter public counter;
function setUp() public { // 每个用例前执行
counter = new Counter();
}
function test_Increment() public {
counter.increment();
assertEq(counter.number(), 1);
}
// 模糊测试:参数 x 由 Foundry 随机生成
function testFuzz_SetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
// 期望回滚
function test_RevertWhen_NotOwner() public {
vm.prank(address(0xBEEF)); // 伪造调用者
vm.expectRevert();
counter.adminOnly();
}
}
常用作弊码:vm.prank(改调用者)、vm.deal(设置余额)、vm.warp(改时间)、vm.expectRevert、vm.expectEmit。
配置文件 foundry.toml:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.24"
optimizer = true
optimizer_runs = 200
二十二.Ethers.js 与 MetaMask 交互
前端通过 Ethers.js (v6) 连接钱包、读写合约。
import { ethers } from "ethers";
// 1.连接 MetaMask
const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []); // 请求连接账户
const signer = await provider.getSigner(); // 签名者(能发交易)
console.log(await signer.getAddress());
// 2.读取链上数据
const balance = await provider.getBalance(await signer.getAddress());
console.log(ethers.formatEther(balance)); // wei -> ETH
// 3.读取合约(只读用 provider)
const counter = new ethers.Contract(address, abi, provider);
console.log(await counter.x());
// 4.写入合约(发交易用 signer)
const counterW = new ethers.Contract(address, abi, signer);
const tx = await counterW.inc();
await tx.wait(); // 等待上链确认
// 5.发送 ETH
await signer.sendTransaction({ to: addr, value: ethers.parseEther("0.01") });
// 6.监听事件
counter.on("Transfer", (from, to, value) => console.log(from, to, value));
// 7.签名消息
const sig = await signer.signMessage("hello");
要点:Provider 只读,Signer 可写;金额用 parseEther / formatEther 转换;发交易后要 wait()。
二十三.项目实战
1.众筹平台:工厂模式 + 状态机 + CEI
enum State { Active, Success, Failed, Closed }
// 工厂:每个众筹活动是一个独立合约
function createCampaign(uint goal, uint days_) external returns (address) {
Campaign c = new Campaign(msg.sender, goal, days_);
return address(c);
}
// 捐款:达到目标自动变成功
function contribute() external payable inState(State.Active) {
require(block.timestamp < deadline, "Ended");
contributions[msg.sender] += msg.value;
totalRaised += msg.value;
if (totalRaised >= goal) state = State.Success;
}
失败时捐款人自行退款(Pull 模式),成功时发起人提现(先改状态再转账)。
2.多签钱包:多个 owner,达到 N 个确认才能执行交易
struct Transaction {
address to;
uint value;
bool executed;
uint confirmations;
}
Transaction[] public txs;
mapping(uint => mapping(address => bool)) public confirmed; // 交易ID => owner => 是否确认
function submit(address to, uint value) public onlyOwner {
txs.push(Transaction(to, value, false, 0)); // 提交
}
function confirm(uint id) public onlyOwner {
require(!confirmed[id][msg.sender], "Done");
confirmed[id][msg.sender] = true; // 确认
txs[id].confirmations++;
}
function execute(uint id) public onlyOwner {
Transaction storage t = txs[id];
require(t.confirmations >= required, "Not enough");
require(!t.executed, "Executed");
t.executed = true; // 先改状态
(bool ok, ) = t.to.call{value: t.value}(""); // 再转账
require(ok);
}
流程:提交 -> 确认 -> 执行(可撤销确认);进阶版本使用代理模式支持升级。
3.NFT 盲盒:ERC721 + Chainlink VRF 随机数 + 白名单分阶段销售
流程:
1. 用户付费购买 -> 铸造盲盒 NFT(未揭示)
2. 请求 Chainlink VRF 随机数(链上不能自己生成安全随机数)
3. VRF 回调 -> 根据随机数分配稀有度
4. 揭示 -> tokenURI 指向 IPFS 上真实元数据
要点:随机数不能用 block.timestamp 等可预测的值;元数据放 IPFS;销售分阶段(未开始 / 白名单 / 公售)。
二十四.知识速查表
存储位置 storage 永久 / memory 临时 / calldata 只读参数
可见性 public / external / internal / private
状态修饰 view 只读 / pure 纯计算 / payable 收款
错误处理 require 校验 / assert 不变量 / revert 抛错 / 自定义 error
转账 call{value: x}("") + require 检查
全局变量 msg.sender / msg.value / block.timestamp / block.number
安全三件套 CEI 模式 / ReentrancyGuard / 权限控制
Gas 优化 打包存储槽 / calldata / 缓存 length / 自定义错误 / 事件代替存储
开发工具 Remix 入门 -> Hardhat(JS 测试) / Foundry(Solidity 测试) -> Ethers.js 前端