MODULE 3 · SOLIDITY 开发
Lesson 7: 合约生命周期与 ABI
📖 阅读 ~15 分钟🧪 3 个实验❓ 2 道测验
合约的一生
一个智能合约从编写到上链,经历这些阶段:
1. 编写 → Solidity 源码 (.sol)
2. 编译 → ABI (接口定义) + Bytecode (字节码)
3. 部署 → 发送 creation tx → 获得合约地址
4. 交互 → 通过 ABI 编码调用函数
5. 升级? → 不可变!(除非用 Proxy 模式)
6. 销毁? → SELFDESTRUCT (已在 Dencun 中禁用)
// 关键事实:
合约一旦部署,代码不可修改
合约地址由部署者地址 + nonce 决定
合约不能主动执行,只能被调用
编译:从源码到字节码
Solidity 编译器 (solc) 将 .sol 文件编译成两个核心产物:
📋 ABI (Application Binary Interface)
- 📝 JSON 格式的接口描述
- 📝 列出所有函数签名和参数类型
- 📝 列出所有事件 (Events)
- 📝 前端/后端用它来编码调用
- 💡 类似 API 文档
⚙️ Bytecode (字节码)
- 📝 EVM 可执行的操作码序列
- 📝 Creation code(含构造函数)
- 📝 Runtime code(链上存储的部分)
- 📝 十六进制字符串 0x6080604...
- 💡 类似编译后的二进制程序
💡 Creation vs Runtime Bytecode
Creation bytecode = 构造函数代码 + runtime code。部署时 EVM 执行 creation code,构造函数运行完毕后,返回 runtime code 存储在链上。之后每次调用合约,执行的是 runtime bytecode。
ABI 编码详解
ABI 定义了如何将函数调用编码为字节序列(calldata),以及如何解码返回值。
// ABI 编码规则:每个参数占 32 字节(右对齐)
// 调用 transfer(address to, uint256 amount)
// to = 0xd8dA6BF2...96045, amount = 1000000 (1 USDC)
a9059cbb ← selector
000000000000000000000000d8da6bf26964af9d7eed9e03 ← address
e53415d37aa96045 (左补零到32B)
0000000000000000000000000000000000000000000000000000 ← uint256
00000000000f4240 (1000000)
// 动态类型 (string, bytes, array) 使用偏移量指针
ABI 编码类型
静态类型
uint256, int256, address, bool, bytes32 等。直接编码在固定位置
每个占 32 字节,右对齐
动态类型
string, bytes, T[], T[k]。使用偏移量 + 长度 + 数据
先放偏移量指针,数据在末尾
abi.encode()
标准编码,补零到 32 字节。用于合约间调用
长度可预测,可解码
abi.encodePacked()
紧凑编码,不补零。用于哈希计算
更短但不能解码,可能碰撞
函数可见性与修饰符
// 四种可见性
public — 任何人都能调用(外部+内部)。自动生成 getter
external — 只能从外部调用。calldata 参数更省 Gas
internal — 只能在合约内部和子合约调用
private — 只能在当前合约内调用(子合约也不行)
// 状态可变性
view — 只读,不修改状态。调用不消耗 Gas(从外部)
pure — 不读也不写状态。纯计算
payable — 可以接收 ETH
(默认) — 可以读写状态,不能接收 ETH
// 常用修饰符
modifier onlyOwner {
require(msg.sender == owner, "Not owner");
_; ← 执行被修饰的函数体
}
🔍 view/pure 的免费调用
从外部调用 view 或 pure 函数是免费的(不需要交易,RPC 节点直接计算返回)。但如果是合约内部调用(另一个写函数调用了 view 函数),仍然消耗 Gas。
Events:链上日志系统
Events 是合约与外部世界通信的关键机制。它们写入交易收据的 logs 中,不占 storage,比 SSTORE 便宜得多。
// 定义事件
event Transfer(
address indexed from, ← indexed: 可被过滤搜索
address indexed to, ← 最多 3 个 indexed 参数
uint256 value ← 非 indexed: 存在 data 中
);
// 触发事件
emit Transfer(msg.sender, to, amount);
// 底层实现:
LOG3 操作码 (3 个 indexed topic + data)
topic[0] = keccak256("Transfer(address,address,uint256)")
topic[1] = from
topic[2] = to
data = abi.encode(value)
💡 为什么 Event 很重要
前端 DApp 通过订阅 Events 来获取实时更新(比轮询 Storage 高效 100 倍)。区块浏览器用 Events 来解析交易。The Graph 等索引器用 Events 构建链上数据库。
🧪 动手实验
🔬 实验 1:查看可用合约
列出 contracts/ 目录下的 Solidity 合约文件。
chainlab lab sol list
观察:每个合约有多少行代码?文件大小如何?
🔬 实验 2:编译合约,看 ABI 和字节码
编译 HelloWorld.sol,观察编译产物:函数列表、事件列表、字节码大小。
chainlab lab sol compile HelloWorld.sol
思考:ABI 中有哪些函数?message() 是什么可见性?setMessage 的选择器是什么?
🔬 实验 3:解码真实 ERC-20 合约调用
用 tool decode 解码一个 ERC-20 transfer 的 calldata。
chainlab tool decode 0xa9059cbb...(transfer calldata)
0xa9059cbb 是哪个函数?参数是什么?
❓ 自测
📝 本课小结
核心要点
1. 合约编译产生 ABI(接口描述)+ Bytecode(可执行代码)
2. 链上只存 runtime bytecode,构造函数只执行一次
3. ABI 编码:静态类型 32B 对齐,动态类型用偏移量指针
4. 四种可见性 (public/external/internal/private) 和状态可变性 (view/pure/payable)
5. Events 是链上日志,比 storage 便宜,是 DApp 获取数据的主要方式
←
下一课
Lesson 8: 代币标准 (ERC-20/721/1155)
→