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 的免费调用 从外部调用 viewpure 函数是免费的(不需要交易,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 是哪个函数?参数是什么?

❓ 自测

检验你的理解

Q1: 部署合约时,链上最终存储的是什么?

✅ 正确!部署交易执行 creation code(运行构造函数),然后将 runtime bytecode 存储在合约地址上。之后的每次调用都执行 runtime code。
❌ 提示:构造函数只运行一次。它执行完后返回的代码才被永久存储。

Q2: 从外部调用一个 view 函数,需要支付 Gas 吗?

✅ 正确!从外部调用 view/pure 函数通过 eth_call RPC,节点在本地模拟执行,不需要发送交易,不消耗 Gas。但合约内部调用 view 函数仍消耗 Gas。
❌ 关键区别:外部调用 view 函数 = eth_call(免费)。合约内部调用 = 消耗 Gas。

📝 本课小结

核心要点 1. 合约编译产生 ABI(接口描述)+ Bytecode(可执行代码)
2. 链上只存 runtime bytecode,构造函数只执行一次
3. ABI 编码:静态类型 32B 对齐,动态类型用偏移量指针
4. 四种可见性 (public/external/internal/private) 和状态可变性 (view/pure/payable)
5. Events 是链上日志,比 storage 便宜,是 DApp 获取数据的主要方式
上一课
Lesson 6: Gas 估算与优化
下一课
Lesson 8: 代币标准 (ERC-20/721/1155)