区块链基础与智能合约开发
1. 区块链核心概念
1.1 区块结构
区块链由一个个按时间顺序串联的区块组成,每个区块包含区块头(Block Header)和区块体(Block Body)。
区块头(Block Header)
| 字段 | 大小 | 说明 |
|---|---|---|
| Version | 4 字节 | 协议版本号 |
| Previous Block Hash | 32 字节 | 前一个区块的哈希指针 |
| Merkle Root | 32 字节 | 区块内所有交易的 Merkle 树根哈希 |
| Timestamp | 4 字节 | 区块生成时间(Unix 时间戳) |
| Difficulty Target | 4 字节 | 难度目标值 |
| Nonce | 4 字节 | 工作量证明的随机数 |
区块体(Block Body)
区块体包含该区块打包的所有交易记录列表。在比特币中,区块体中的交易以 Merkle 树(哈希二叉树)的形式组织。
Merkle 树
Merkle 树是一种哈希二叉树,叶子节点是每笔交易的哈希值,非叶子节点是其子节点哈希值的串联再哈希。Merkle 树具有以下特性:
- 可以高效验证某笔交易是否包含在区块中(O(log n) 时间复杂度)
- 轻节点只需保存区块头和 Merkle 根,无需下载完整区块数据即可验证交易
- 任意一笔交易的修改都会传递到 Merkle 根,确保交易不可篡改
Merkle Root
/ \
Hash12 Hash34
/ \ / \
Hash1 Hash2 Hash3 Hash4
| | | |
Tx1 Tx2 Tx3 Tx41.2 链式结构
区块链的核心在于通过哈希指针将区块串联成链。
创世块(Genesis Block)
创世块是区块链的第一个区块,没有前驱区块,其 Previous Block Hash 字段通常为全零。创世块由协议硬编码在每个节点中,是整条链的起点。
前后哈希指针
每个区块的区块头中都包含了前一个区块的哈希值(Previous Block Hash),这种设计使得:
- 一旦某个区块的数据被篡改,该区块的哈希值会改变,后续所有区块的哈希指针都会断裂
- 篡改者需要重新计算从该区块之后的所有区块的工作量证明(PoW 场景下),成本极高
- 区块链越长,历史数据越安全
Genesis Block <-- Block 1 <-- Block 2 <-- Block 3
(Hash: 0x00) (Prev: 0x00) (Prev: 0x1A) (Prev: 0x2B)1.3 共识机制
共识机制是区块链网络中所有节点就数据状态达成一致的规则。
| 共识机制 | 原理 | 优点 | 缺点 | 典型应用 |
|---|---|---|---|---|
| PoW(工作量证明) | 节点计算满足难度条件的 Nonce,第一个找到的节点获得记账权 | 去中心化程度高、安全性极强 | 能耗巨大、TPS 低(比特币约 7 TPS)、确认时间长 | Bitcoin、早期 Ethereum |
| PoS(权益证明) | 节点根据持有代币数量和时间(币龄)获得记账权,验证者质押代币 | 能耗低、TPS 较高(以太坊约 100+ TPS) | 富者愈富(马太效应)、Nothing at Stake 问题 | Ethereum 2.0(合并后) |
| DPoS(委托权益证明) | 持币人投票选举若干见证人(Witness)节点负责出块 | TPS 高(可达数千)、能耗低 | 一定程度中心化、见证人可能合谋 | EOS、TRON |
| PBFT(实用拜占庭容错) | 需要在 3f+1 个节点中容忍 f 个拜占庭节点,经过三阶段协议(Pre-Prepare/Prepare/Commit)达成共识 | 低延迟、最终确认 | 通信复杂度 O(n^2)、节点数量受限 | Hyperledger Fabric、FISCO BCOS |
| Raft | 通过 Leader 选举和日志复制实现共识,非拜占庭容错 | 实现简单、高性能 | 无法容忍拜占庭错误 | 联盟链场景、私有链 |
1.4 密码学基础
SHA-256 哈希
SHA-256(Secure Hash Algorithm 256-bit)是区块链中使用最广泛的哈希算法,具有以下特性:
- 单向性:从输入计算哈希值容易,从哈希值反推输入在计算上不可行
- 抗碰撞性:找到两个不同输入具有相同哈希值在计算上不可行
- 雪崩效应:输入微小的变化会导致输出完全不同的哈希值
// Solidity 中计算哈希
bytes32 hash = keccak256(abi.encodePacked(data));比特币使用双重 SHA-256:SHA256(SHA256(data))。
椭圆曲线 ECDSA
ECDSA(Elliptic Curve Digital Signature Algorithm)是区块链中使用的非对称加密算法。比特币和以太坊都使用 secp256k1 曲线。
- 私钥:随机生成的 256 位二进制数
- 公钥:通过椭圆曲线乘法从私钥派生(K = k * G,其中 G 是椭圆曲线基点)
- 从公钥无法反向推导私钥(椭圆曲线离散对数问题)
数字签名流程
- 签名方使用私钥对消息哈希进行签名,生成 (r, s) 签名对
- 验证方使用签名方的公钥验证签名
// 使用 ethers.js 签名和验证
const { Wallet } = require('ethers');
const wallet = Wallet.createRandom();
// 签名
const message = "Hello Blockchain";
const signature = await wallet.signMessage(message);
// 验证
const recoveredAddress = ethers.verifyMessage(message, signature);
console.log(recoveredAddress === wallet.address); // trueBase58 编码
Base58 是一种用于加密货币地址的编码方案,去除了 Base64 中易混淆的字符(0/O/I/l 等),包含 58 个字符(A-Z 去掉了 O/I,a-z 去掉了 l,加上数字 1-9)。
比特币地址使用 Base58Check 编码,包含版本字节和校验和(双重 SHA-256 的前 4 字节)。
Base58 字符集: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz2. 区块链类型
2.1 公有链(Public Blockchain)
公有链是完全开放、去中心化的区块链网络,任何人可以自由加入、参与共识、读取和发送交易。
Bitcoin
- 2008 年中本聪发表比特币白皮书,2009 年创世块诞生
- 共识机制:PoW
- 区块时间:约 10 分钟
- 代币总量:2100 万 BTC
- 脚本语言:Bitcoin Script(非图灵完备)
- 应用场景:数字黄金、价值存储
Ethereum
- 2015 年由 Vitalik Buterin 创建
- 共识机制:PoW -> PoS(2022 年合并)
- 区块时间:约 12 秒
- 智能合约:Solidity 语言(图灵完备)
- 应用场景:DeFi、NFT、DAO、DApp
2.2 联盟链(Consortium Blockchain)
联盟链由多个机构共同参与管理,节点需要授权才能加入。
Hyperledger Fabric
- 由 Linux 基金会主导
- 支持智能合约(Chaincode,用 Go/Java/Node.js 编写)
- 共识可插拔(支持 Raft、Kafka 等)
- 支持私有数据和通道(Channel)实现数据隔离
- 不支持原生代币
FISCO BCOS
- 由微众银行牵头研发的国产联盟链平台
- 支持 PBFT 和 Raft 共识
- 提供群组架构实现数据隔离
- 内置 Gas 管理但可关闭
- 支持 Solidity 智能合约
Quorum
- 基于以太坊的企业级联盟链
- 支持隐私交易和私有状态
- 使用 Raft 或 Istanbul BFT 共识
- 适合金融场景
2.3 私有链(Private Blockchain)
私有链的写入权限完全由单一组织控制,适用于企业内部场景。与传统数据库的区别在于区块链提供了不可篡改和可追溯的特性。
2.4 跨链技术
中继链(Relay Chain)
通过一个中间链连接多个区块链。每条链与中继链通信,由中继链负责跨链消息的验证和转发。
- 典型案例:Polkadot(中继链 + 平行链)、Cosmos(Hub + Zone)
- 优点:安全性高,去中心化
- 缺点:实现复杂
哈希锁定(Hash Time-Locked Contract, HTLC)
通过哈希锁和时间锁实现跨链原子交换。双方在约定时间内提供哈希原像即可完成交换。
侧链(Sidechain)
侧链是与主链(Mainchain)平行运行的独立区块链,通过双向锚定(Two-way Peg)实现资产互通。
跨链桥(Cross-Chain Bridge)
跨链桥是连接两个独立区块链的协议,允许资产和数据在链间转移。常见类型:
- 锁定-铸造型:在源链锁定资产,在目标链铸造等价资产
- 流动性网络型:通过流动性提供者实现跨链兑换
2.5 区块链类型对比
| 特性 | 公有链 | 联盟链 | 私有链 |
|---|---|---|---|
| 节点加入 | 无需许可 | 授权加入 | 单一机构控制 |
| 共识机制 | PoW / PoS | PBFT / Raft | 自定义 |
| 去中心化程度 | 完全去中心化 | 部分去中心化 | 中心化 |
| 交易速度 | 低(7~100 TPS) | 高(千~万 TPS) | 极高 |
| 数据可见性 | 公开 | 成员可见 | 内部可见 |
| 典型场景 | 加密资产、DeFi | 供应链、金融结算 | 内部审计 |
3. 以太坊与 EVM
3.1 以太坊发展历程
| 阶段 | 时间 | 关键特性 |
|---|---|---|
| Frontier | 2015-07 | 初始版本,命令行交互 |
| Homestead | 2016-03 | 提升安全性,引入 Mist 钱包 |
| Metropolis Byzantium | 2017-10 | 隐私优化,难度炸弹延迟 |
| Metropolis Constantinople | 2019-02 | 降低 Gas 费用,优化存储 |
| Istanbul | 2019-12 | 兼容 ZK-SNARKs,优化网络 |
| Berlin | 2021-04 | Gas 计费优化 |
| London | 2021-08 | EIP-1559(费用市场改革) |
| Merge | 2022-09 | PoW -> PoS 共识转换 |
| Shanghai | 2023-04 | 启用质押提款(EIP-4895) |
| Cancun | 2024-03 | EIP-4844(Proto-Danksharding) |
3.2 账户模型
以太坊使用基于账户的模型(Account Model),而非比特币的 UTXO 模型。
EOA(Externally Owned Account)外部账户
- 由私钥控制
- 可以创建合约、发送交易
- 包含字段:nonce、balance
- 地址由公钥哈希的后 20 字节派生
合约账户(Contract Account)
- 由合约代码控制
- 由 EOA 或其他合约创建
- 包含字段:nonce、balance、codeHash、storageRoot
- 被交易触发时执行代码
// 账户相关操作
address public owner;
uint256 public balance;
function getBalance() public view returns (uint256) {
return address(this).balance; // 合约自身余额
}
function transferEth(address payable to) public {
to.transfer(1 ether); // 转账 1 ETH
}nonce 和 balance
- nonce:账户发送的交易计数(EOA)或合约创建的合约计数(合约账户),用于防止重放攻击
- balance:账户持有的 ETH 余额,单位为 wei(1 ETH = 10^18 wei)
3.3 Gas 机制
Gas 是以太坊中衡量执行交易或智能合约所需计算资源的单位。
Gas Limit 与 Gas Price
- Gas Limit:用户愿意为交易支付的最大 Gas 量
- Gas Price:每单位 Gas 的价格(单位为 Gwei,1 Gwei = 10^9 wei)
- 交易费用 = Gas Used Gas Price
EIP-1559(伦敦升级)
EIP-1559 引入了新的费用结构:
- Base Fee(基础费):由协议根据网络拥堵程度自动调整,最多可增减 12.5%,会被销毁
- Priority Fee(小费/优先费):给矿工/验证者的激励
- Max Fee:用户愿意支付的最高费用(Max Fee = Base Fee + Priority Fee 的上限)
- 用户可设置 Max Priority Fee
交易费用 = Base Fee Gas Used + Priority Fee Gas Used常见操作的 Gas 消耗
| 操作 | Gas 消耗 |
|---|---|
| 简单转账 | 21,000 |
| 部署合约 | 200,000 ~ 数千万 |
| ERC-20 transfer | ~50,000 |
| ERC-721 transfer | ~100,000 |
| SSTORE(存储写入) | 20,000(初始)/ 5,000(修改)/ 返还 |
| SLOAD(存储读取) | 2,100 |
3.4 交易结构
以太坊交易的核心字段:
{
"nonce": "0x01",
"from": "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B",
"to": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
"value": "0x1bc16d674ec80000", // 2 ETH in wei
"data": "0x", // 合约调用时包含函数选择器和参数
"gasLimit": "0x5208", // 21000
"maxPriorityFeePerGas": "0x59682f00", // 1.5 Gwei
"maxFeePerGas": "0xee6b2800", // 4 Gwei
"signature": {
"v": "0x1b",
"r": "0x...",
"s": "0x..."
}
}from:发送方地址(由签名恢复)to:接收方地址(EOA 为转账,合约地址为调用)value:发送的 ETH 数量(wei)data:合约创建时的字节码或合约调用时的 calldatasignature:由发送方私钥签名的 (v, r, s) 值
合约调用时的 data 编码
函数选择器(4 字节) + 参数编码(每 32 字节对齐)
例如调用 transfer(address,uint256) 到 0xAb...9B 转账 1000:
0xa9059cbb -- transfer 函数选择器(keccak256 前 4 字节)
000000000000000000000000Ab...9B -- address 填充为 32 字节
00000000000000000000000000000000000000000000000000000000000003e8 -- 10004. 智能合约开发(Solidity)
4.1 合约结构
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
// 状态变量
address public owner;
uint256 public totalMinted;
// 事件
event Minted(address indexed to, uint256 amount);
// modifier
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
// 构造函数
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
owner = msg.sender;
}
// 函数
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
totalMinted += amount;
emit Minted(to, amount);
}
// 回退函数
receive() external payable {}
}关键组成
pragma:指定 Solidity 编译器版本import:导入其他合约或库contract:合约定义,类似面向对象编程中的类- 状态变量(State Variables):永久存储在链上
modifier:函数修饰器,用于复用访问控制等逻辑event:事件,用于日志记录和 DApp 前端监听
4.2 数据类型
值类型
| 类型 | 说明 | 示例 |
|---|---|---|
bool | 布尔值 | bool public isActive = true; |
uint | 无符号整数(默认 uint256) | uint256 public count = 100; |
int | 有符号整数(默认 int256) | int256 public balance = -50; |
address | 20 字节以太坊地址 | address public owner; |
address payable | 可接收 ETH 的地址 | address payable public receiver; |
bytes1 ~ bytes32 | 固定长度字节数组 | bytes32 public hash; |
引用类型
| 类型 | 说明 | 示例 |
|---|---|---|
string | 变长 UTF-8 字符串 | string public name = "Token"; |
bytes | 变长字节数组 | bytes public data; |
mapping | 键值映射(类似哈希表) | mapping(address => uint256) public balances; |
struct | 自定义数据结构 | struct User { address addr; uint256 age; } |
| 数组 | 固定或变长数组 | uint256[] public numbers; |
// 复杂数据类型示例
struct Voter {
uint256 weight;
bool voted;
address delegate;
uint256 vote;
}
mapping(address => Voter) public voters;
address[] public voterList;
function addVoter(address _addr, uint256 _weight) public {
voters[_addr] = Voter({
weight: _weight,
voted: false,
delegate: address(0),
vote: 0
});
voterList.push(_addr);
}4.3 函数修饰符
| 修饰符 | 可见性 | 说明 |
|---|---|---|
public | 任何 | 合约内外均可调用 |
internal | 仅合约和子合约 | 外部不可调用(默认) |
external | 仅外部 | 只能从合约外部调用,内部用 this.f() 调用 |
private | 仅合约 | 子合约不可见 |
状态可变性
| 修饰符 | 说明 |
|---|---|
view | 读取但不修改状态 |
pure | 既不读取也不修改状态(仅依赖函数参数) |
payable | 允许接收 ETH |
// 状态可变性示例
uint256 public value;
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // 仅使用参数,不访问状态
}
function getValue() public view returns (uint256) {
return value; // 读取状态但不修改
}
function setValue(uint256 _value) public {
value = _value; // 修改状态
}
function deposit() public payable {
// payable 允许接收 ETH
}4.4 继承与接口
继承
Solidity 支持多重继承,使用 is 关键字。
// 基础合约
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
}
// 子合约继承
contract MyContract is Ownable {
uint256 public value;
function setValue(uint256 _value) external onlyOwner {
value = _value;
}
// 重写父合约函数
// 父合约函数需标记为 virtual
// 子合约函数需使用 override
}接口与抽象合约
// 接口(所有函数隐式为 external)
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// 抽象合约(包含未实现的函数)
abstract contract BaseToken {
function mint(address to, uint256 amount) public virtual;
function burn(uint256 amount) public virtual;
}
// 实现接口
contract MyToken is IERC20 {
function totalSupply() external view override returns (uint256) {
// 实现逻辑
}
// ... 实现其他接口函数
}4.5 错误处理
| 函数 | 说明 | 是否返还 Gas | 适用场景 |
|---|---|---|---|
require(条件, "错误信息") | 条件不满足时回退 | 是 | 输入验证、权限检查 |
revert("错误信息") | 无条件回退 | 是 | 复杂条件判断 |
assert(条件) | 条件不满足时回退 | 否 | 内部错误、不变性检查 |
function withdraw(uint256 amount) external {
// require:权限和条件验证
require(msg.sender == owner, "Unauthorized");
require(amount <= balances[msg.sender], "Insufficient balance");
require(amount > 0, "Amount must be > 0");
// 状态变更
balances[msg.sender] -= amount;
// revert:复杂条件判断
if (amount > maxWithdrawLimit) {
revert("Exceeds withdrawal limit");
}
// assert:内部不变性检查
assert(balances[msg.sender] <= totalSupply);
}Solidity 0.8+ 内置错误与 try/catch
// 自定义错误(推荐,比字符串更省 Gas)
error InsufficientBalance(uint256 available, uint256 required);
error Unauthorized(address caller);
function transfer(address to, uint256 amount) public {
if (msg.sender == address(0)) {
revert Unauthorized(msg.sender);
}
if (balanceOf[msg.sender] < amount) {
revert InsufficientBalance(balanceOf[msg.sender], amount);
}
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
}
// try/catch 处理外部调用失败
function safeCall(address target) external {
try ITarget(target).someFunction() {
// 调用成功
} catch Error(string memory reason) {
// require 类型的回退
} catch (bytes memory lowLevelData) {
// 无错误消息的回退
}
}5. 常见合约模式
5.1 ERC-20 代币标准
ERC-20 是以太坊上最广泛使用的同质化代币标准(ERC-20: Token Standard)。
核心接口
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}关键操作
transfer:从调用者地址直接转账approve:授权 spender 可以从调用者账户转移 amount 数量的代币transferFrom:授权的 spender 将代币从 from 转移到 toallowance:查询 owner 授权给 spender 的额度
// 使用 OpenZeppelin 创建 ERC-20 代币
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
_mint(msg.sender, initialSupply);
}
}5.2 ERC-721 NFT 标准
ERC-721 是非同质化代币(NFT)标准,每个代币独一无二(ERC-721: Non-Fungible Token Standard)。
interface IERC721 {
function balanceOf(address owner) external view returns (uint256);
function ownerOf(uint256 tokenId) external view returns (address);
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address approved, uint256 tokenId) external;
function setApprovalForAll(address operator, bool approved) external;
function getApproved(uint256 tokenId) external view returns (address);
function isApprovedForAll(address owner, address operator) external view returns (bool);
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
}关键函数
balanceOf:查询地址拥有的 NFT 数量ownerOf:查询 NFT 的拥有者safeTransferFrom:安全转移 NFT(目标地址为合约时检查是否支持 ERC-721)tokenURI(非接口强制,但约定实现):返回 NFT 的元数据 URI
// OpenZeppelin 实现 ERC-721
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721, Ownable {
uint256 private _nextTokenId;
constructor() ERC721("MyNFT", "MNFT") {}
function safeMint(address to) public onlyOwner {
uint256 tokenId = _nextTokenId;
_nextTokenId++;
_safeMint(to, tokenId);
}
function _baseURI() internal pure override returns (string memory) {
return "https://api.mynft.com/metadata/";
}
}5.3 ERC-1155 多代币标准
ERC-1155 是一种多代币标准,可以在一个合约中管理同质化代币、非同质化代币和半同质化代币(ERC-1155: Multi Token Standard)。
interface IERC1155 {
function balanceOf(address account, uint256 id) external view returns (uint256);
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
external view returns (uint256[] memory);
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address account, address operator) external view returns (bool);
function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external;
function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external;
}批量转账
// ERC-1155 支持批量转账,节省 Gas
uint256[] memory ids = new uint256[](2);
ids[0] = 1;
ids[1] = 2;
uint256[] memory amounts = new uint256[](2);
amounts[0] = 100;
amounts[1] = 1; // tokenId 2 是 NFT(数量为 1)
contract.safeBatchTransferFrom(from, to, ids, amounts, "");5.4 Ownable(权限管理)
Ownable 合约提供了基础的所有权管理功能,是最常用的访问控制模式。
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is Ownable {
uint256 public importantValue;
// onlyOwner 修饰符限制只有 owner 可调用
function setImportantValue(uint256 _value) external onlyOwner {
importantValue = _value;
}
// 转移所有权
// 调用 transferOwnership(newOwner) 即可
// 放弃所有权(合约变为不可管理状态)
// 调用 renounceOwnership(),owner 变为 address(0)
}AccessControl(基于角色的访问控制)
对于更复杂的权限管理,OpenZeppelin 提供了 AccessControl 合约:
import "@openzeppelin/contracts/access/AccessControl.sol";
contract MyContract is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
// 只有 MINTER_ROLE 可铸造
}
}5.5 ReentrancyGuard(防重入锁)
ReentrancyGuard 用于防止重入攻击(将在安全章节详细讨论)。
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SecureWithdraw is ReentrancyGuard {
mapping(address => uint256) public balances;
// nonReentrant 防止函数在执行期间被重入
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// nonReentrant 确保以下代码不能被重入
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
}6. 开发与部署工具
6.1 Remix IDE
Remix 是基于浏览器的 Solidity 开发 IDE(https://remix.ethereum.org)。
主要功能
- Solidity 代码编辑器(语法高亮、代码补全)
- 编译(支持多个编译器版本)
- 部署(JavaScript VM / 测试网 / 主网注入)
- 调试器(逐行调试、查看状态变量)
- 静态分析(安全性检查)
6.2 Hardhat
Hardhat 是以太坊的主流开发框架,支持编译、测试、部署和调试。
安装与初始化
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat initHardhat 配置
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: {
version: "0.8.20",
settings: {
optimizer: { enabled: true, runs: 200 },
},
},
networks: {
sepolia: {
url: "https://rpc.sepolia.org",
accounts: [process.env.PRIVATE_KEY],
},
hardhat: {
// 本地开发网络,自动提供 20 个测试账户
},
},
};编译
npx hardhat compile编译产物在 artifacts/ 目录,包含 ABI 和 Bytecode。
编写与运行测试
// test/MyToken.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyToken", function () {
let token, owner, addr1;
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const MyToken = await ethers.getContractFactory("MyToken");
token = await MyToken.deploy(1000000);
await token.waitForDeployment();
});
it("should have correct name and symbol", async function () {
expect(await token.name()).to.equal("MyToken");
expect(await token.symbol()).to.equal("MTK");
});
it("should transfer tokens", async function () {
await token.transfer(addr1.address, 100);
expect(await token.balanceOf(addr1.address)).to.equal(100);
});
});部署脚本
// scripts/deploy.js
const hre = require("hardhat");
async function main() {
const MyToken = await hre.ethers.getContractFactory("MyToken");
const token = await MyToken.deploy(1000000);
await token.waitForDeployment();
console.log("MyToken deployed to:", await token.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});npx hardhat run scripts/deploy.js --network sepoliaconsole.log 调试
Hardhat 支持在 Solidity 中使用 console.log 进行调试(仅 Hardhat 本地网络支持):
import "hardhat/console.sol";
contract DebugDemo {
function calculate(uint256 a, uint256 b) public view returns (uint256) {
console.log("a:", a, "b:", b);
uint256 result = a + b;
console.log("result:", result);
return result;
}
}6.3 OpenZeppelin 合约库
OpenZeppelin 提供了经过审计的智能合约库,是 Solidity 开发的事实标准。
npm install @openzeppelin/contracts常用模块
@openzeppelin/contracts/token/ERC20/ERC20.sol- ERC-20 实现@openzeppelin/contracts/token/ERC721/ERC721.sol- ERC-721 实现@openzeppelin/contracts/token/ERC1155/ERC1155.sol- ERC-1155 实现@openzeppelin/contracts/access/Ownable.sol- 所有权管理@openzeppelin/contracts/access/AccessControl.sol- 基于角色的访问控制@openzeppelin/contracts/utils/ReentrancyGuard.sol- 重入攻击防护@openzeppelin/contracts/utils/Pausable.sol- 暂停/恢复合约@openzeppelin/contracts/utils/cryptography/MerkleProof.sol- Merkle 证明验证
6.4 测试网
| 测试网 | 类型 | 共识 | 说明 |
|---|---|---|---|
| Sepolia | 公共测试网 | PoS | 当前以太坊官方推荐测试网 |
| Goerli | 公共测试网(已弃用) | PoS | 已逐步退出使用 |
| Holesky | 公共测试网 | PoS | 面向共识层的测试网 |
| Hardhat | 本地网络 | - | 本地开发环境,无需等待确认 |
获取测试网 ETH
- Sepolia:通过 Sepolia Faucet(如 https://sepoliafaucet.com/)领取
- 本地开发:Hardhat 自动提供 10000 ETH 的测试账户
6.5 合约部署流程
Solidity 代码 --> 编译 --> ABI + Bytecode
|
部署交易:from + to(空) + data(Bytecode) + value(0)
|
等待交易确认(区块打包)
|
获取合约地址(由 from + nonce 计算)
|
验证合约(Etherscan)合约地址计算
合约地址由部署者地址和 nonce 通过 RLP 编码和 keccak256 哈希计算:
contractAddress = keccak256(RLP.encode([deployerAddress, nonce]))[12:]6.6 前端集成
ethers.js
import { ethers } from "ethers";
// 连接 MetaMask
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// 合约 ABI 和地址
const contractAddress = "0x...";
const abi = [...]; // 从 artifacts 或 Etherscan 获取
// 创建合约实例
const contract = new ethers.Contract(contractAddress, abi, signer);
// 读取数据(view/pure 函数)
const balance = await contract.balanceOf(signer.address);
// 写入数据(需发送交易)
const tx = await contract.transfer("0x...", ethers.parseEther("1.0"));
await tx.wait(); // 等待交易确认
console.log("Transaction confirmed:", tx.hash);Web3.js
const Web3 = require("web3");
const web3 = new Web3(window.ethereum || "http://localhost:8545");
const contract = new web3.eth.Contract(abi, contractAddress);
// 读取
const balance = await contract.methods.balanceOf(account).call();
// 写入
const tx = await contract.methods.transfer(to, amount).send({ from: account });7. 智能合约安全
7.1 重入攻击(Reentrancy)
重入攻击是最经典的智能合约漏洞之一。攻击者利用合约在更新状态之前进行外部调用的时机,递归调用目标函数窃取资产。
攻击原理
// 漏洞合约
contract VulnerableWithdraw {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0);
// 漏洞:先转账后更新状态
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0; // 这行在外部调用之后执行
}
}
// 攻击合约
contract Attacker {
VulnerableWithdraw public vulnerable;
constructor(address _vulnerable) {
vulnerable = VulnerableWithdraw(_vulnerable);
}
// fallback 函数在接收到 ETH 时被调用,再次触发 withdraw
receive() external payable {
if (address(vulnerable).balance >= 1 ether) {
vulnerable.withdraw();
}
}
function attack() external payable {
vulnerable.withdraw();
}
}解决方案一:Checks-Effects-Interactions 模式
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0);
// Effect:先更新状态
balances[msg.sender] = 0;
// Interaction:后执行外部调用
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}解决方案二:ReentrancyGuard
使用 OpenZeppelin 的 ReentrancyGuard 锁:
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SafeWithdraw is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0);
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
}7.2 整数溢出
在 Solidity 0.8 之前,整数运算默认会回绕(wrap around)。0.8+ 版本内置了溢出检查。
// Solidity 0.8+ 自动检查溢出
uint8 a = 255;
uint8 b = 1;
uint8 c = a + b; // Solidity 0.8+ 会 revert,0.7- 会得到 0
// 使用 unchecked 可以禁用溢出检查(节省 Gas)
function addUnchecked(uint256 x, uint256 y) public pure returns (uint256) {
unchecked {
return x + y; // 不检查溢出
}
}SafeMath 库(Solidity 0.8 之前使用)
// OpenZeppelin SafeMath(Solidity 0.6-0.7 版本)
using SafeMath for uint256;
function safeAdd(uint256 a, uint256 b) public pure returns (uint256) {
return a.add(b); // 溢出时 revert
}7.3 前端抢跑(Front-running)
在公开的区块链网络中,交易的排序由矿工/验证者决定。攻击者可以观察待处理交易池(Mempool),抢先提交交易获利。
常见攻击形式
- Front-running:攻击者监测到目标交易后,支付更高 Gas 将类似交易排在前面
- Forex/抢跑:在 DEX 交易中,看到大额买单后抢先买入,等价格上涨后卖出
- 尾随交易(Back-running):攻击者的交易紧随目标交易之后执行
Commit-Reveal 方案
// Commit-Reveal 模式防止抢跑
contract CommitReveal {
struct Commit {
bytes32 hash;
uint256 timestamp;
}
mapping(address => Commit) public commits;
function commit(bytes32 hash) external {
commits[msg.sender] = Commit(hash, block.timestamp);
}
function reveal(string memory secret) external {
Commit memory c = commits[msg.sender];
require(keccak256(abi.encodePacked(secret)) == c.hash, "Invalid secret");
// 执行实际逻辑
}
}其他防护措施
- 使用私有交易池(如 Flashbots)
- 设置滑点保护
- 使用恒定平均值或时间加权价格预言机
7.4 访问控制漏洞
不正确的权限管理可能导致未授权操作。
// 危险:没有权限控制
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
// 安全:使用 OpenZeppelin 的 Ownable
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
// 安全:使用 AccessControl
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}常见问题
tx.origin和msg.sender混淆(tx.origin不应作为权限验证依据)- 构造函数名称错误(Solidity 0.4.x 中使用合约同名构造函数而非
constructor) selfdestruct导致合约被强制销毁
// 危险:使用 tx.origin
function withdraw() external {
require(tx.origin == owner); // 可通过中间合约绕过
// ...
}
// 安全:使用 msg.sender
function withdraw() external {
require(msg.sender == owner);
// ...
}7.5 闪电贷攻击
闪电贷(Flash Loan)是 DeFi 中的一种无抵押贷款,借款人必须在同一笔交易中借入并归还资金。攻击者利用闪电贷在短时间内获得大量资金,操控市场价格。
攻击原理
- 通过闪电贷借入大量资金
- 使用借入资金在 DEX 上执行大额交易,操控价格
- 利用被扭曲的价格在其他协议中套利或清算
- 归还闪电贷,净赚差价
防护措施
- 使用去中心化预言机(如 Chainlink)的时间加权价格,而非 DEX 的瞬时价格
- 对价格变化幅度进行限制
- 使用 TWAP(Time-Weighted Average Price)预言机
// 使用 Chainlink 预言机获取防操纵的价格
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract PriceConsumer {
AggregatorV3Interface internal priceFeed;
constructor() {
// ETH/USD 预言机地址(Sepolia 测试网)
priceFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306);
}
function getLatestPrice() public view returns (uint256) {
(, int256 price, , , ) = priceFeed.latestRoundData();
return uint256(price);
}
}安全审计检查清单
| 检查项 | 说明 |
|---|---|
| 重入攻击 | 遵循 Checks-Effects-Interactions 模式或使用 ReentrancyGuard |
| 整数溢出 | 使用 Solidity 0.8+ 或 SafeMath |
| 访问控制 | 正确使用 msg.sender、Ownable、AccessControl |
| 前端抢跑 | 使用 Commit-Reveal、滑点保护、私有交易池 |
| 预言机操纵 | 使用去中心化预言机、TWAP 价格 |
| 闪电贷攻击 | 价格限制、TWAP 预言机 |
| 未检查的外部调用 | 检查 call/delegatecall 返回值 |
| 时间戳操纵 | 不依赖 block.timestamp 作为随机数来源 |
| 逻辑缺陷 | 完整的单元测试和形式化验证 |
| Gas 限制 | 避免循环中的动态数组操作 |