1. Solidity 概览
Solidity 是 Ethereum 及 EVM 兼容链上编写智能合约的主力语言。它是一种静态类型、面向合约的语言,语法借鉴了 JavaScript、Python 和 C++,但运行模型与传统编程有本质差异------每一次状态变更都需要消耗 Gas,且合约一旦部署便不可修改。
理解 Solidity 的关键心智模型转变:
- 一切皆交易 :合约函数调用本质上是一笔链上交易(
view/pure除外) - 状态是昂贵的:读写 storage 是 Gas 消耗的最大头
- 信任边界不同:合约的调用者可能是任何人,甚至是恶意合约
- 不可变性:部署后代码无法修改,需要通过代理模式等架构手段解决
版本与编译
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
Solidity 0.8.0 是一个重要分界线:内置了算术溢出检查(之前需要 SafeMath 库)。当前(2024-2025)生产环境推荐使用 ^0.8.20 或更高版本。
2. 开发环境
推荐工具链
| 工具 | 定位 | 适用场景 |
|---|---|---|
| Hardhat | JS/TS 生态的全栈框架 | 前端集成、快速原型 |
| Foundry | Rust 编写的高性能框架 | 专业合约开发、 fuzz 测试 |
| Remix IDE | 浏览器 IDE | 学习、快速实验 |
| OpenZeppelin | 合约标准库 | 生产级安全组件 |
Hardhat 快速上手
bash
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat --init
项目结构:
bash
├── contracts/ # Solidity 源码
├── scripts/ # 部署脚本
├── test/ # 测试文件
├── hardhat.config.js
└── package.json
Foundry 快速上手
bash
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init my-project
Foundry 的核心优势在于极快的编译和测试速度,以及内置的 fuzz testing 能力。
3. 数据类型与变量
值类型(Value Types)
值类型在赋值时总是复制一份,独立于原始值。
solidity
// 整数:支持多种位宽
uint256 public balance; // 无符号 256 位,默认类型
int128 public delta; // 有符号 128 位
uint8 public counter; // 0 ~ 255
// 布尔
bool public active = true;
// 地址
address public owner;
address payable public recipient; // 可接收 ETH 的地址
// 定长字节数组
bytes32 public hash; // 32 字节,常用于哈希值
bytes20 public addr; // 20 字节,与 address 同大小,但需显式转换
// 枚举(底层是 uint,从 0 开始)
enum Status { Pending, Active, Completed, Cancelled }
Status public currentStatus = Status.Pending;
关键点 :uint256 是 EVM 的原生字长,使用更小的类型(如 uint8)在 storage 打包时有优化效果,但在函数参数和局部变量中反而更贵(需要额外的清理操作)。
引用类型(Reference Types)
引用类型赋值时传递引用,修改会影响原始数据。必须明确标注数据位置。
solidity
// 动态数组
uint256[] public numbers;
address[] public participants;
// 定长数组
uint256[10] public fixedNumbers;
// 结构体
struct User {
address addr;
uint256 balance;
Status status;
}
// 映射(不能遍历,不能有 key 列表)
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;
// 字符串与字节
string public name; // 动态 UTF-8 字符串
bytes public data; // 动态字节数组
数据位置:storage / memory / calldata
这是 Solidity 独有的概念,直接影响 Gas 消耗和性能。
solidity
contract DataLocation {
uint256[] public storageArray; // state 变量永远是 storage
function example(uint256[] calldata input) external {
// calldata: 只读,不复制,Gas 最低 ------ 用于外部函数参数
uint256 first = input[0];
// memory: 复制到内存,可修改,函数结束后销毁
uint256[] memory copy = input;
copy[0] = 999;
// storage 引用:指向 state 变量,修改会持久化
uint256[] storage ref = storageArray;
ref.push(42); // 直接修改 storageArray
}
}
经验法则:
- 外部函数的只读参数用
calldata - 需要修改的临时数据用
memory - 需要持久化的数据用
storage(注意避免不必要的 storage 读写)
4. 函数系统
可见性(Visibility)
solidity
function externalFunc() external { } // 只能从合约外部调用
function publicFunc() public { } // 内外皆可
function internalFunc() internal { } // 仅内部和子合约
function privateFunc() private { } // 仅当前合约
external 函数接收参数通过 calldata,比 public 的 memory 复制更省 Gas。因此如果一个函数只预期被外部调用,优先使用 external。
特殊函数修饰
solidity
// view: 读取状态但不修改,不消耗 Gas(外部调用时)
function getBalance() external view returns (uint256) {
return address(this).balance;
}
// pure: 不读不写状态,纯计算
function add(uint256 a, uint256 b) external pure returns (uint256) {
return a + b;
}
// payable: 允许接收 ETH
function deposit() external payable {
balances[msg.sender] += msg.value;
}
函数修改器(Modifiers)
修改器是 Solidity 的 AOP(面向切面编程)机制,常用于权限控制和前置条件校验。
solidity
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_; // 占位符,表示被修饰函数的执行位置
}
modifier validAmount(uint256 amount) {
require(amount > 0, "Amount must be > 0");
_;
}
function withdraw(uint256 amount) external onlyOwner validAmount(amount) {
payable(msg.sender).transfer(amount);
}
返回值
solidity
// 命名返回值
function getInfo() external view returns (address addr, uint256 bal) {
addr = msg.sender;
bal = balances[msg.sender];
}
// 多返回值与解构
function swap(uint256 a, uint256 b) external pure returns (uint256, uint256) {
return (b, a);
}
5. 合约结构与面向对象
继承
Solidity 支持多重继承,使用 C3 线性化解决菱形继承问题。
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
}
contract Pausable {
bool public paused;
modifier whenNotPaused() {
require(!paused, "Paused");
_;
}
}
// 多重继承: Ownable + Pausable
contract Token is Ownable, Pausable {
mapping(address => uint256) public balances;
function mint(address to, uint256 amount) external onlyOwner whenNotPaused {
balances[to] += amount;
}
// 继承顺序决定了修饰器优先级,也可以显式指定
// Ownable.owner 是 public state 变量,自动生成同名 getter,无需手动 override
}
抽象合约与接口
solidity
// 抽象合约:至少有一个未实现的函数
abstract contract ERC20Base {
function totalSupply() public view virtual returns (uint256);
function balanceOf(address) public view virtual returns (uint256);
}
// 接口:所有函数都是外部且未实现的,不能有状态变量
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);
event Transfer(address indexed from, address indexed to, uint256 value);
}
实践建议:优先使用接口而非抽象合约,接口不占用合约大小限制(24KB),且解耦更彻底。
库(Libraries)
库是部署在链上的可复用代码,使用 delegatecall 执行(修改调用合约的状态),或对于 internal/private 函数直接内联编译进调用合约。
solidity
library SafeTransfer {
function safeTransfer(address token, address to, uint256 amount) internal {
(bool success, bytes memory data) = token.call(
abi.encodeWithSignature("transfer(address,uint256)", to, amount)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), "Transfer failed");
}
}
contract Vault {
using SafeTransfer for address;
function rescueToken(address token, uint256 amount) external {
token.safeTransfer(msg.sender, amount);
}
}
6. 高级特性
事件(Events)
事件是合约向链外(前端、索引服务)通信的唯一低成本方式。数据存储在日志中,合约自身无法读取。
solidity
contract Auction {
// indexed 参数可以被高效过滤(最多 3 个)
event BidPlaced(address indexed bidder, uint256 amount, uint256 timestamp);
event AuctionEnded(address indexed winner, uint256 highestBid);
function bid() external payable {
emit BidPlaced(msg.sender, msg.value, block.timestamp);
}
}
自定义错误(Custom Errors)
0.8.4 引入,比 require(message) 更省 Gas。原因是:require 的字符串参数会完整编码进 revert 数据(越长的字符串消耗越多),而自定义错误只编码 4 字节的错误选择器加参数,大幅减少 revert 数据量。
solidity
error InsufficientBalance(uint256 available, uint256 required);
error Unauthorized(address caller);
contract Vault {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
if (balances[msg.sender] < amount) {
revert InsufficientBalance(balances[msg.sender], amount);
}
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
低级调用与合约交互
solidity
contract Caller {
// call: 调用外部合约,最推荐的方式
function callExternal(address target, uint256 value) external {
(bool success, bytes memory returnData) = target.call{value: value}(
abi.encodeWithSignature("deposit(uint256)", 100)
);
require(success, "External call failed");
}
// delegatecall: 在调用者的上下文中执行目标合约的代码
// 用于代理模式(Proxy Pattern)
function upgradeCall(address implementation, bytes memory data) external {
(bool success, ) = implementation.delegatecall(data);
require(success, "Delegatecall failed");
}
// staticcall: 只读调用,如果目标修改状态则会 revert
function readOnly(address target) external view returns (bytes memory) {
(bool success, bytes memory data) = target.staticcall(
abi.encodeWithSignature("getInfo()")
);
require(success);
return data;
}
}
call vs delegatecall:
| 特性 | call | delegatecall |
|---|---|---|
| 代码执行位置 | 目标合约 | 调用合约 |
| msg.sender | 调用合约地址 | 原始调用者 |
| storage | 目标合约的 storage | 调用合约的 storage |
| 用途 | 普通合约交互 | 代理模式、库 |
delegatecall 安全警告 :delegatecall 在调用合约的 storage 上下文中执行,因此调用合约和逻辑合约的 storage 布局必须严格一致(变量声明顺序、类型完全相同),否则会导致状态变量被错误覆盖。此外,逻辑合约必须被初始化(通过 initialize() 而非构造函数),且需防止被他人抢先初始化。
try/catch 错误处理
对外部调用使用 try/catch 可以让合约优雅地处理失败,而非整体回滚。
solidity
contract ResilientVault {
IERC20 public token;
constructor(address _token) {
token = IERC20(_token);
}
function safeTransfer(address to, uint256 amount) external {
try token.transfer(to, amount) returns (bool success) {
if (!success) {
// 处理 transfer 返回 false 的情况(如 USDT 等非标准实现)
emit TransferFailed(to, amount);
}
} catch Error(string memory reason) {
// 捕获 require/revert 带字符串的情况
emit TransferReverted(reason);
} catch (bytes memory) {
// 捕获所有其他异常(如目标合约不存在、Gas 不足等)
emit UnknownError();
}
}
event TransferFailed(address to, uint256 amount);
event TransferReverted(string reason);
event UnknownError();
}
构造器与特殊函数
solidity
contract MyContract {
address public owner; // 普通 state 变量
address public immutable deployer; // immutable: 构造器中赋值一次,之后不可变
uint256 public constant MAX_SUPPLY = 1_000_000; // constant: 编译时常量,直接内联
constructor(address _initialOwner) {
deployer = msg.sender;
owner = _initialOwner;
}
// receive: 接收纯 ETH 转账(无 calldata)
receive() external payable { }
// fallback: 处理不匹配任何函数的调用
fallback() external payable { }
}
两者的 Gas 优化机制略有不同:constant 在编译时直接将值替换到字节码中;immutable 在部署时写入合约的运行时代码(而非 storage),因此读取时通过 CODECOPY 而非 SLOAD,Gas 成本远低于普通 storage 变量。两者都不占用 storage slot。
7. 安全实战
这是智能合约开发中最关键的部分。合约一旦部署,漏洞可能直接导致资金损失且无法挽回。
重入攻击(Reentrancy)
攻击原理 :合约在更新余额前将 ETH 发送给攻击者,攻击者的 receive() 函数再次调用取款函数,形成递归。
solidity
// ❌ 危险代码
contract VulnerableVault {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] = 0; // 这行在重入时还未执行!
}
}
// ✅ 修复方案:Checks-Effects-Interactions 模式
contract SafeVault {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender]; // Checks
require(amount > 0, "No balance");
balances[msg.sender] = 0; // Effects(先更新状态)
(bool ok, ) = msg.sender.call{value: amount}(""); // Interactions
require(ok, "Transfer failed");
}
}
// ✅ 修复方案 2:重入锁
contract SafeVault2 {
uint256 private locked = 1;
modifier nonReentrant() {
require(locked == 1, "Reentrant call");
locked = 2;
_;
locked = 1;
}
function withdraw() external nonReentrant {
// ...
}
}
前端运行(Front-running)
问题:矿工/验证者可以看到 mempool 中的待处理交易,并据此插入自己的交易获利。
防御措施:
- 使用 commit-reveal 方案(先提交哈希,后揭示数据)
- 设置滑点保护(DEX 中设置
minAmountOut) - 使用 Flashbots 等私有交易池
访问控制缺失
solidity
// ❌ 任何人都能调用
function setFee(uint256 newFee) external {
fee = newFee;
}
// ✅ 正确的访问控制
function setFee(uint256 newFee) external onlyOwner {
require(newFee <= MAX_FEE, "Fee too high");
fee = newFee;
emit FeeUpdated(newFee);
}
tx.origin 钓鱼
solidity
// ❌ 危险:如果用户被诱导调用恶意合约,tx.origin 是用户地址
require(tx.origin == owner, "Not owner");
// ✅ 正确:msg.sender 是直接调用者
require(msg.sender == owner, "Not owner");
未检查的外部调用返回值
solidity
// ❌ send 和 transfer 失败只返回 false 或抛少量 Gas
// 注意:transfer 固定 2300 gas 限制,在 Gas 成本变化后可能失败
(bool ok, ) = recipient.call{value: amount}("");
// 忘记检查 ok...
// ✅ 始终检查返回值
(bool ok, ) = recipient.call{value: amount}("");
require(ok, "Transfer failed");
整数溢出/下溢
Solidity 0.8+ 已内置检查,但使用 unchecked 块时需格外小心:
solidity
// unchecked 块可节省 Gas,但必须先完成边界校验
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
unchecked {
// 前置 require 已保证 amount <= balances[msg.sender],减法不会下溢
// 同时 amount <= 2^256,加法也不会溢出(余额上限约束)
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
8. Gas 优化
核心原则
Gas 消耗的主要来源按数量级排列:
- SSTORE(写入 storage):~20,000 gas(新 slot)/ ~5,000 gas(更新)
- SLOAD(读取 storage):~2,100 gas(冷访问)/ ~100 gas(热访问)
- CALL(外部调用):~2,600 gas + 子调用 Gas
- 内存操作和计算:通常可忽略
实用优化技巧
1. 变量打包(Struct Packing)
EVM storage slot 为 32 字节,连续的小变量可以打包进同一个 slot。
solidity
// ❌ 浪费:每个变量占一个 slot(共 3 slots)
uint8 a;
uint256 b;
uint8 c;
// ✅ 打包:a 和 c 与 b 共享一个 slot(共 2 slots)
uint8 a;
uint8 c;
uint256 b;
// ✅ 使用 struct 显式打包
struct Packed {
uint128 balance; // 16 bytes
uint64 timestamp; // 8 bytes
uint32 id; // 4 bytes
bool active; // 1 byte
} // 总计 29 bytes,一个 slot 搞定
2. 用 calldata 代替 memory
solidity
// ❌ 复制到 memory 需要额外 Gas
function process(uint256[] memory data) external { }
// ✅ calldata 直接读取,零拷贝
function process(uint256[] calldata data) external { }
3. 使用 constant / immutable
solidity
// ❌ 每次读取消耗 storage gas
uint256 public maxSupply = 1000000;
// ✅ constant 编译时替换,零 Gas
uint256 public constant MAX_SUPPLY = 1000000;
// ✅ immutable 构造时确定,首次读取后缓存
address public immutable DEPLOYER = msg.sender;
4. 短路运算
solidity
// 将更可能为 false 的条件放前面
require(cheapCheck() && expensiveCheck(), "Failed");
5. 合理使用 unchecked
solidity
// 循环计数器用 unchecked 可以显著省 Gas
for (uint256 i = 0; i < array.length;) {
// ... process array[i]
unchecked { ++i; } // 不会溢出(除非数组长度达到 2^256)
}
9. ERC 代币标准
ERC(Ethereum Request for Comments)是以太坊生态的合约接口标准,定义了不同场景下合约必须实现的函数,确保互操作性。
ERC-20:同质化代币
最常用的代币标准,定义了 totalSupply、balanceOf、transfer、transferFrom、approve、allowance 六个核心函数。几乎所有 DeFi 协议都基于此标准交互。
solidity
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1_000_000 * 10 ** decimals());
}
}
ERC-721:非同质化代币(NFT)
每个代币有唯一的 tokenId,适合数字艺术品、游戏资产、域名等场景。核心操作包括 ownerOf、safeTransferFrom、approve。
ERC-1155:多代币标准
一个合约中同时管理多种代币(同质化和非同质化混合),支持批量转账,大幅降低 Gas 成本。广泛用于游戏资产、批量空投。
ERC-4626:代币化金库
定义了 ERC-20 金库的标准接口,统一了存入、提取、份额计算的函数签名。Yearn、Aave 等协议的 Vault 正在向此标准迁移。
实践建议:生产环境中应始终使用 OpenZeppelin 的合约实现而非从零编写,并在其基础上通过继承添加自定义逻辑(如投票、白名单、版税等)。
10. 设计模式
代理模式(Proxy Pattern)
解决合约不可变性问题。用户与代理合约交互,代理通过 delegatecall 转发调用到逻辑合约。
solidity
// 简化版透明代理
contract Proxy {
address public implementation;
address public admin;
constructor(address _impl) {
implementation = _impl;
admin = msg.sender;
}
// 管理员升级
function upgradeTo(address newImpl) external {
require(msg.sender == admin, "Not admin");
implementation = newImpl;
}
// 所有其他调用转发到逻辑合约
fallback() external payable {
address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
生产环境请使用 OpenZeppelin 的 TransparentUpgradeableProxy 或 UUPSUpgradeable,它们处理了 storage 冲突、初始化等复杂问题。
工厂模式(Factory Pattern)
solidity
contract TokenFactory {
event TokenCreated(address indexed token, string name, address creator);
function createToken(string calldata name, string calldata symbol) external returns (address) {
SimpleToken token = new SimpleToken(name, symbol, msg.sender);
emit TokenCreated(address(token), name, msg.sender);
return address(token);
}
}
使用 CREATE2 可以预计算合约地址(常用于空投、Layer 2 钱包):
solidity
function deployDeterministic(bytes32 salt, bytes memory bytecode) internal returns (address addr) {
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
}
拉取支付模式(Pull Over Push)
避免在向多个接收方转账时因某个地址 revert 而导致全部失败。
solidity
// ❌ Push 模式:一个失败全部失败
function distributeRewards() external {
for (uint256 i = 0; i < recipients.length; i++) {
payable(recipients[i]).transfer(rewards[recipients[i]]);
}
}
// ✅ Pull 模式:用户自己来取
mapping(address => uint256) public pendingRewards;
function claimReward() external {
uint256 reward = pendingRewards[msg.sender];
require(reward > 0, "No reward");
pendingRewards[msg.sender] = 0;
payable(msg.sender).transfer(reward);
}
11. 测试与部署
Hardhat 测试示例
javascript
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Vault", function () {
let vault, owner, user;
beforeEach(async function () {
[owner, user] = await ethers.getSigners();
const Vault = await ethers.getContractFactory("Vault");
vault = await Vault.deploy();
await vault.waitForDeployment();
});
it("should allow deposits and withdrawals", async function () {
await vault.connect(user).deposit({ value: ethers.parseEther("1.0") });
expect(await vault.balances(user.address)).to.equal(ethers.parseEther("1.0"));
await vault.connect(user).withdraw(ethers.parseEther("0.5"));
expect(await vault.balances(user.address)).to.equal(ethers.parseEther("0.5"));
});
it("should revert on insufficient balance", async function () {
await expect(
vault.connect(user).withdraw(ethers.parseEther("1.0"))
).to.be.revertedWithCustomError(vault, "InsufficientBalance");
});
});
Foundry 测试示例
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Vault.sol";
contract VaultTest is Test {
Vault public vault;
address public user = address(0x1);
function setUp() public {
vault = new Vault();
vm.deal(user, 10 ether);
}
function test_DepositAndWithdraw() public {
vm.prank(user);
vault.deposit{value: 1 ether}();
assertEq(vault.balances(user), 1 ether);
vm.prank(user);
vault.withdraw(0.5 ether);
assertEq(vault.balances(user), 0.5 ether);
}
// Fuzz testing: 自动随机生成输入
function testFuzz_Deposit(uint256 amount) public {
amount = bound(amount, 0, 10 ether);
vm.deal(user, amount);
vm.prank(user);
vault.deposit{value: amount}();
assertEq(vault.balances(user), amount);
}
}
部署脚本
javascript
// scripts/deploy.js (Hardhat)
async function main() {
const Vault = await ethers.getContractFactory("Vault");
const vault = await Vault.deploy();
await vault.waitForDeployment();
console.log("Vault deployed to:", await vault.getAddress());
}
main().catch(console.error).finally(() => process.exit());
部署命令:
bash
# Hardhat - 部署到测试网
npx hardhat run scripts/deploy.js --network sepolia
# Foundry - 部署并验证
forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast --verify
12. 实用速查表
全局变量
| 变量 | 说明 |
|---|---|
msg.sender |
当前调用者地址 |
msg.value |
本次调用携带的 ETH(wei) |
msg.data |
完整 calldata |
block.timestamp |
当前区块时间戳(可被矿工小幅操纵) |
block.number |
当前区块号 |
block.chainid |
链 ID |
tx.origin |
交易发起者(勿用于鉴权) |
gasleft() |
剩余 Gas |
address(this) |
当前合约地址 |
ETH 转账方式对比
| 方式 | Gas 限制 | 失败处理 | 推荐程度 |
|---|---|---|---|
transfer() |
2300 固定 | revert | 不推荐 |
send() |
2300 固定 | 返回 bool | 不推荐 |
call{value: x}("") |
全部可用 Gas | 返回 bool | 推荐 |
注意 :使用 call{value:}("") 时务必配合重入锁(见第 7 节),因为它会将所有剩余 Gas 转发给接收方,接收方可能借此执行回调代码发起重入攻击。
常用内建函数
solidity
// 地址
addr.balance; // 查询 ETH 余额
addr.call{value: 1 ether}(""); // 低级调用
addr.delegatecall(data); // 代理调用
// ABI 编码
abi.encode(a, b); // 标准编码
abi.encodePacked(a, b); // 紧凑编码(注意碰撞风险)
abi.encodeWithSignature("func(uint256)", x);
abi.encodeWithSelector(IERC20.transfer.selector, to, amount);
// 哈希
keccak256(abi.encodePacked(a, b)); // Keccak-256 哈希
// 错误处理
require(condition, "message"); // 条件校验,失败时回滚并退还剩余 Gas
revert CustomError(params); // 自定义错误(revert 数据更短,比 require 省 Gas)
assert(condition); // 不变量检查(正常逻辑不应失败)
13. 推荐学习路径
第一阶段:语言基础(1-2 周)
掌握数据类型、函数、继承、事件等核心语法。在 Remix IDE 中编写简单合约(投票、存储、ERC20 代币)建立直觉。
第二阶段:安全与模式(2-3 周)
系统学习常见漏洞(重入、前端运行、访问控制),精读 Ethernaut 和 Damn Vulnerable DeFi 等 CTF 挑战。这是从"能写"到"写得好"的关键跨越。
第三阶段:工程实践(2-4 周)
掌握 Hardhat 或 Foundry 的完整开发流程,学习编写全面的测试(单元测试 + fuzz 测试),理解 Gas 优化策略,精读 OpenZeppelin 合约库的实现。
第四阶段:深入 EVM(持续)
理解 Yul/内联汇编、storage 布局、合约大小限制、EIP 标准(ERC-20/721/1155/4626 等),阅读 DeFi 头部项目的源码(Uniswap V3、Aave、Compound)。
推荐资源:
- 官方文档:soliditylang.org
- 交互学习:cryptozombies.io
- 安全练习:ethernaut.openzeppelin.com
- CTF 挑战:github.com/tinchoabbate/damn-vulnerable-defi
- 合约范例:github.com/OpenZeppelin/openzeppelin-contracts
本文档基于 Solidity 0.8.x 版本编写,最后更新于 2026 年 6 月。