我最近在重新学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,并且方便我们在合约中使用函数选择器和事件哈希。