我最近在重新学Huff,巩固一下细节,也写一个“Huff极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。
所有代码和教程开源在github: github.com/AmazingAng/WTF-Huff
这一讲,我们将介绍Huff中的接口,它可以用来生成Solidity接口合约/ABI,并且方便我们在合约中使用函数选择器(function selector)和事件哈希(event hash)。
接口
类似Solidity,你可以在Huff合约的接口中定义函数functions
,事件events
,和错误errors
。接口主要有两个作用:
- 定义接口后,函数名可以用作内置函数
__FUNC_SIG
(获取函数选择器),__EVENT_HASH
(事件选择器),和__ERROR
(错误选择器)的参数 - 生成 Solidity 接口/合约 ABI。
接口中的函数可以是view
、pure
、payable
或nonpayable
类型。并且,只有外部可见的函数需要在接口中定义,内部函数不需要。接口中的事件可以包含索引值(使用indexed
关键字)和非索引值。
Huff接口的例子:
Simple Store合约
现在,让我们重温第一讲中介绍的Simple Store
合约。学到这里,你应该能看懂它了。
我们把合约分为两部分,第一部分定义了合约的接口,存储槽,并用宏实现了接口中定义的SET_VALUE()
和GET_VALUE
方法。
-
SET_VALUE()
: 先使用calldataload
从calldata
读出了新值,然后使用sstore
将值保存在存储槽VALUE_LOCATION
中。注意,第一行0x04 calldataload
读取值的时候略去了前4
字节,因为它们是函数选择器。 -
GET_VALUE()
: 先使用sload
读取存储槽VALUE_LOCATION
的值,使用mstore
将值存入内存,再使用return
返回。
注意,一定要确保每个方法被正确的结束,代码以
return
,revert
,stop
,invalid
指令结尾,不然可能会有漏洞。
第二部分是Main宏,合约的主入口,判断外部调用的是哪个函数。
-
第一行,我们使用
0x00 calldataload 0xE0 shr
读取calldata
中前4
字节,也就是函数选择器。这段代码我们会经常使用,你可以想一想它是怎么工作的。 -
获取
selector
后,我们要通过比对setValue()
和getValue()
进行跳转,如果没有匹配的函数,则revert
。由于我们在接口中定义了这两个函数,我们可以使用内置函数__FUNC_SIG()
获取他们的selector
并推入堆栈,然后使用eq
进行比对。不然的话,就要使用__FUNC_SIG("function setValue(uint256) nonpayable returns ()")
,很繁琐。 -
在
set
和get
两个跳转标签之后,我们分别运行SET_VALUE()
和GET_VALUE()
方法,执行相应的逻辑。
输出Solidity接口/ABI
我们可以使用huffc -g
命令将Huff合约的接口转为Solidity合约接口/ABI:
输出的接口将保存在和07_Interface.huff
相同的文件夹下,例如src/I07_Iterface.sol
,内容:
分析合约字节码
我们可以使用huffc
命令获取上面合约的runtime code:
打印出的bytecode为:
转换成格式化的表格:
pc | op | opcode | stack |
---|---|---|---|
[00] | 5f | PUSH0 | 0x00 |
[01] | 35 | CALLDATALOAD | calldata |
[02] | 60e0 | PUSH1 0xE0 | 0xE0 calldata |
[04] | 1c | SHR | selector |
[05] | 80 | DUP1 | selector selector |
[06] | 63 55241077 | PUSH4 0x55241077 | 0x55241077 selector selector |
[0a] | 14 | EQ | suc selector |
[0b] | 61 001e | PUSH2 0x001E | 0x001E suc selector |
[0e] | 57 | JUMPI | selector |
[0f] | 80 | DUP1 | selector selector |
[10] | 63 209652 | PUSH4 0x20965255 | 0x20965255 selector selector |
[14] | 14 | EQ | suc selector |
[15] | 61 0024 | PUSH2 0x0024 | 0x0024 suc selector |
[18] | 57 | JUMPI | selector |
[19] | 5f | PUSH0 | 0x00 selector |
[1a] | 5f | PUSH0 | 0x00 0x00 selector |
[1b] | fd | REVERT | selector |
[1c] | 5b | JUMPDEST | selector |
[1d] | 60 04 | PUSH1 0x04 | 0x04 selector |
[1f] | 35 | CALLDATALOAD | calldata@0x04 selector |
[20] | 5f | PUSH0 | 0x00 calldata@0x04 selector |
[21] | 55 | SSTORE | selector |
[22] | 00 | STOP | selector |
[23] | 5b | JUMPDEST | selector |
[24] | 5f | PUSH0 | 0x00 selector |
[25] | 54 | SLOAD | value selector |
[26] | 5f | PUSH0 | 0x00 value selector |
[27] | 52 | MSTORE | selector |
[28] | 60 20 | PUSH1 0x20 | 0x20 selector |
[2a] | 5f | PUSH0 | 0x00 0x20 selector |
[2b] | f3 | RETURN | selector |
我们可以看到,这段字节码的功能:
- 使用
CALLDATALOAD
从calldata
中读取值,然后使用SHR
获取前4
字节的函数选择器。 - 用
EQ
对比calldata
中的函数选择器是否为0x55241077
或0x20965255
,若匹配,则将PC跳转到相应的JUMPDEST
,执行SET_VALUE()
或GET_VALUE()
方法。
总结
这一讲,我们介绍了Huff中的接口,它可以用来生成Solidity接口合约/ABI,并且方便我们在合约中使用函数选择器和事件哈希。