Solidity 合约高级应用 8| 简单字节码合约 (Simple Bytecode Contract)
在 Solidity 中,合约最终会被编译为 EVM 字节码部署到链上。理解字节码有助于深入掌握合约的底层行为、优化 Gas 以及编写极简合约。下面将介绍字节码的基本构成,并展示如何编写一个“简单字节码合约”——即通过直接操作字节码实现最小功能的合约。
1. 合约字节码的构成
一个合约的字节码分为两部分:
- 创建时字节码(Init Code / Constructor Bytecode):在部署交易中执行的代码。它负责初始化状态变量、执行构造函数逻辑,并最终返回运行时字节码(Runtime Bytecode)。创建时字节码执行完毕后,其返回的代码将永久存储在链上,成为合约的代码。
- 运行时字节码(Runtime Bytecode):存储在链上、在每次调用合约时执行的代码。它包含了合约的函数选择器、各函数的实现逻辑等。
例如,一个简单的存储合约:
pragma solidity ^0.8.0;
contract SimpleStorage {
uint256 public storedData;
function set(uint256 x) public {
storedData = x;
}
function get() public view returns (uint256) {
return storedData;
}
}
编译后,你会得到两段字节码:创建时字节码(包含构造函数和返回运行时字节码的逻辑)和运行时字节码(包含 set 和 get 的实现)。
2. 极简运行时字节码:仅返回一个值
如果我们想要一个最简单的合约,它只做一件事:每次调用都返回一个固定的数字(比如 42),那么它的运行时字节码可以非常短。
2.1 用 Solidity 实现
pragma solidity ^0.8.0;
contract SimpleReturn {
function always42() external pure returns (uint256) {
return 42;
}
}
编译后,这个合约的运行时字节码大约有几十个字节,包含了函数选择器、跳转表等。但我们可以做得更小:直接编写一个合约,使得任何调用(无论 calldata 是什么)都返回 42。
2.2 使用内联汇编编写极简运行时字节码
我们可以直接在 Solidity 合约的构造函数中返回我们自定义的运行时字节码。下面的合约在部署时不会包含任何 Solidity 生成的函数,而是直接返回一段手写的汇编字节码:
pragma solidity ^0.8.0;
contract TinyBytecode {
constructor() {
// 在构造函数中返回自定义的运行时字节码
bytes memory runtimeCode = hex"600a600c600039600a6000f3602a60005260206000f3";
assembly {
return(add(runtimeCode, 0x20), mload(runtimeCode))
}
}
}
但这段代码比较晦涩。更清晰的方式是使用 Yul 对象直接编写字节码。实际上,我们可以创建一个只包含运行时字节码的“原始”合约,用 create 或 create2 部署。这里给出一个完整的 Yul 示例:
object "SimpleReturn" {
code {
// 部署时代码:直接返回 runtime 对象
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
code {
// 运行时:永远返回 42 (0x2a)
// 将 0x2a 放入内存,然后返回
mstore(0, 0x2a)
return(0, 0x20)
}
}
}
将上述 Yul 编译后,可以得到一个极小的合约:它的运行时字节码为 602a60005260206000f3。解释一下:
602a:将0x2a(42) 压入栈。600052:将栈顶值(42)存到内存地址 0。6020:将0x20(32) 压入栈(返回值长度)。6000:将0压入栈(返回值起始内存地址)。f3:RETURN,从内存返回数据。
所以任何调用该合约的交易,无论 calldata 是什么,都会返回 0x0000...002a(即 32 字节的 42)。
3. 直接部署原始字节码
如果你已经有一个编译好的运行时字节码(例如上面得到的 602a60005260206000f3),你可以通过一个“部署器”合约来部署它,或者直接使用 create 操作码。
3.1 使用 Solidity 的 create 部署
pragma solidity ^0.8.0;
contract Deployer {
function deployTiny() external returns (address) {
bytes memory runtimeCode = hex"602a60005260206000f3";
address addr;
assembly {
addr := create(0, add(runtimeCode, 0x20), mload(runtimeCode))
}
require(addr != address(0), "deploy failed");
return addr;
}
}
调用 deployTiny 会创建一个新合约,其代码就是 602a60005260206000f3。然后你可以调用该合约(用任意 calldata),它总是返回 42。
3.2 直接通过交易部署
你也可以手动构造一个部署交易,其 data 字段包含创建时字节码。创建时字节码需要包含将运行时字节码返回的逻辑。例如,使用如下 Yul 对象生成创建时字节码:
object "Creator" {
code {
// 复制运行时字节码到内存并返回
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
code {
// 运行时字节码(和上面一样)
mstore(0, 0x2a)
return(0, 0x20)
}
}
}
编译后得到创建时字节码,直接作为交易的 data 发送,即可部署出只返回 42 的合约。
4. 为什么需要简单字节码合约?
- 最小化合约:用于特定目的(如预言机返回固定值、多签钱包的 fallback 函数)可以大大降低 Gas 成本。
- 教学用途:帮助理解 EVM 执行模型。
- 高级优化:某些协议需要极小的代理合约(如 EIP-1167 最小代理合约),其字节码通常只有 45 字节左右。
5. 风险与注意事项
- 可读性差:直接操作字节码容易出错且难以审计。
- 函数选择器缺失:上述极简合约没有函数选择器逻辑,任何调用都会返回相同结果。如果需要多个函数,必须手动实现跳转表。
- 存储与内存:手动管理内存和存储需谨慎,避免破坏合约状态。
- 部署方式:使用
create部署时,要确保字节码正确,否则可能部署失败或产生意外行为
6. 扩展:EIP-1167 最小代理合约
一个更实用的“简单字节码合约”是 EIP-1167 最小代理,它通过极少的字节码将调用转发给另一个实现合约。其运行时字节码只有 45 字节:
363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3
7. 总结
简单字节码合约展示了 Solidity 和 EVM 的底层能力。通过直接编写或部署极简的运行时字节码,可以创建功能单一但 Gas 极低的合约。理解字节码结构不仅有助于优化,也能帮助开发者深入理解合约的执行过程。在实际开发中,大多数情况下应使用高级语言(Solidity、Yul)确保安全,但掌握底层知识是成为高级开发者的重要一步。
更多推荐



所有评论(0)