跳到主要内容

WTF Huff极简入门: 07. 接口

我最近在重新学Huff,巩固一下细节,也写一个“Huff极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。

推特:@0xAA_Science

社区:Discord微信群官网 wtf.academy

所有代码和教程开源在github: github.com/AmazingAng/WTF-Huff


这一讲,我们将介绍Huff中的接口,它可以用来生成Solidity接口合约/ABI,并且方便我们在合约中使用函数选择器(function selector)和事件哈希(event hash)。

接口

类似Solidity,你可以在Huff合约的接口中定义函数functions,事件events,和错误errors。接口主要有两个作用:

  1. 定义接口后,函数名可以用作内置函数__FUNC_SIG(获取函数选择器),__EVENT_HASH(事件选择器),和__ERROR(错误选择器)的参数
  2. 生成 Solidity 接口/合约 ABI。

接口中的函数可以是viewpurepayablenonpayable类型。并且,只有外部可见的函数需要在接口中定义,内部函数不需要。接口中的事件可以包含索引值(使用indexed关键字)和非索引值。

Huff接口的例子:

#define function testFunction(uint256, bytes32) view returns (bytes memory)

#define event TestEvent(address indexed, uint256)

Simple Store合约

现在,让我们重温第一讲中介绍的Simple Store合约。学到这里,你应该能看懂它了。

我们把合约分为两部分,第一部分定义了合约的接口,存储槽,并用宏实现了接口中定义的SET_VALUE()GET_VALUE方法。

  • SET_VALUE(): 先使用calldataloadcalldata读出了新值,然后使用sstore将值保存在存储槽VALUE_LOCATION中。注意,第一行0x04 calldataload读取值的时候略去了前4字节,因为它们是函数选择器。

  • GET_VALUE(): 先使用sload读取存储槽VALUE_LOCATION的值,使用mstore将值存入内存,再使用return返回。

注意,一定要确保每个方法被正确的结束,代码以returnrevertstopinvalid指令结尾,不然可能会有漏洞。

/* 接口 */
#define function setValue(uint256) nonpayable returns ()
#define function getValue() view returns (uint256)

/* 存储槽位 */
#define constant VALUE_LOCATION = FREE_STORAGE_POINTER()

/* 方法 */
#define macro SET_VALUE() = takes (0) returns (0) {
0x04 calldataload // [value]
[VALUE_LOCATION] // [ptr, value]
sstore // []
stop // []
}

#define macro GET_VALUE() = takes (0) returns (0) {
// 从存储中加载值
[VALUE_LOCATION] // [ptr]
sload // [value]

// 将值存入内存
0x00 mstore

// 返回值
0x20 0x00 return
}

第二部分是Main宏,合约的主入口,判断外部调用的是哪个函数。

// 合约的主入口,判断调用的是哪个函数
#define macro MAIN() = takes (0) returns (0) {
// 通过selector判断要调用哪个函数
0x00 calldataload 0xE0 shr
dup1 __FUNC_SIG(setValue) eq set jumpi
dup1 __FUNC_SIG(getValue) eq get jumpi
// 如果没有匹配的函数,就revert
0x00 0x00 revert

set:
SET_VALUE()
get:
GET_VALUE()
}
  1. 第一行,我们使用0x00 calldataload 0xE0 shr读取calldata中前4字节,也就是函数选择器。这段代码我们会经常使用,你可以想一想它是怎么工作的。

  2. 获取selector后,我们要通过比对setValue()getValue()进行跳转,如果没有匹配的函数,则revert。由于我们在接口中定义了这两个函数,我们可以使用内置函数__FUNC_SIG()获取他们的selector并推入堆栈,然后使用eq进行比对。不然的话,就要使用__FUNC_SIG("function setValue(uint256) nonpayable returns ()"),很繁琐。

  3. setget两个跳转标签之后,我们分别运行SET_VALUE()GET_VALUE()方法,执行相应的逻辑。

输出Solidity接口/ABI

我们可以使用huffc -g命令将Huff合约的接口转为Solidity合约接口/ABI:

huffc src/07_Interface.huff -g

输出的接口将保存在和07_Interface.huff相同的文件夹下,例如src/I07_Iterface.sol,内容:

interface I07_Interface {
function getValue() external view returns (uint256);
function setValue(uint256) external;
}

分析合约字节码

我们可以使用huffc命令获取上面合约的runtime code:

huffc src/07_Interface.huff -r

打印出的bytecode为:

5f3560e01c8063552410771461001e5780632096525514610025575f5ffd5b6004355f55005b5f545f5260205ff3

转换成格式化的表格:

pcopopcodestack
[00]5fPUSH00x00
[01]35CALLDATALOADcalldata
[02]60e0PUSH1 0xE00xE0 calldata
[04]1cSHRselector
[05]80DUP1selector selector
[06]63 55241077PUSH4 0x552410770x55241077 selector selector
[0a]14EQsuc selector
[0b]61 001ePUSH2 0x001E0x001E suc selector
[0e]57JUMPIselector
[0f]80DUP1selector selector
[10]63 209652PUSH4 0x209652550x20965255 selector selector
[14]14EQsuc selector
[15]61 0024PUSH2 0x00240x0024 suc selector
[18]57JUMPIselector
[19]5fPUSH00x00 selector
[1a]5fPUSH00x00 0x00 selector
[1b]fdREVERTselector
[1c]5bJUMPDESTselector
[1d]60 04PUSH1 0x040x04 selector
[1f]35CALLDATALOADcalldata@0x04 selector
[20]5fPUSH00x00 calldata@0x04 selector
[21]55SSTOREselector
[22]00STOPselector
[23]5bJUMPDESTselector
[24]5fPUSH00x00 selector
[25]54SLOADvalue selector
[26]5fPUSH00x00 value selector
[27]52MSTOREselector
[28]60 20PUSH1 0x200x20 selector
[2a]5fPUSH00x00 0x20 selector
[2b]f3RETURNselector

我们可以看到,这段字节码的功能:

  1. 使用CALLDATALOADcalldata中读取值,然后使用SHR获取前4字节的函数选择器。
  2. EQ对比calldata中的函数选择器是否为0x552410770x20965255,若匹配,则将PC跳转到相应的JUMPDEST,执行SET_VALUE()GET_VALUE()方法。

总结

这一讲,我们介绍了Huff中的接口,它可以用来生成Solidity接口合约/ABI,并且方便我们在合约中使用函数选择器和事件哈希。