12.String


文档摘要

title: 12. String tags: huff interface string bytecode WTF Huff极简入门: 12. String 我最近在重新学Huff,巩固一下细节,也写一个“Huff极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAAScience 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTF-Huff Huff并不原生支持字符串(string),这一讲,我们介绍如何在Huff中使用String,包括将 保存到状态变量和在函数中返回它。

title: 12. String tags: - huff - interface - string - bytecode

WTF Huff极简入门: 12. String

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

推特:@0xAA_Science

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

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

Huff并不原生支持字符串(string),这一讲,我们介绍如何在Huff中使用String,包括将string保存到状态变量和在函数中返回它。

String

在Solidity合约中,我们经常会用到字符串类型的变量,比如ERC20标准中的symbolname都是字符串。Huff并不原生支持string类型,但是我们可以根据Solidity的存储布局和ABI标准,在Huff中实现它。

要实现的Solidity合约:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.15; contract String { string str; function setString(string memory _str) external{ str = _str; } function getString() external view returns(string memory _str){ _str = str; } }

string类型进阶

要在Huff中实现string类型,我们必须先学习它是如何在EVM的存储、calldata,和returndata中布局的。

存储布局

在Solidity中,stringbytes存储的方式相同。

  • 对于短字节/字符串(长度<=31字节),字符串的内容和长度会存到同一个slot中,就像存储bytes32uint256那样,从而节省gas。字符串的内容从最高阶字节开始存,而字符串长度的两倍(len * 2)则会存储在最低阶的字节中。比如WTF长度为3,它的ASCII值为575446,在存储槽中表示为5754460000000000000000000000000000000000000000000000000000000006

  • 对于长字节/字符串(长度>31字节),存储方式与动态数组相同。假设我们开始从存储槽0开始存储长字符串,则slot 0仅存储字符串的长度 * 2 + 1,这是为了方便和短字符串分辨。那么字符串的实际内容存在哪个存储槽呢?这个存储槽由slot 0keccak256哈希值决定,也就是存储槽keccak256(0),如果一个存储槽不够,接下来的内容将存储在keccak256(0) + 1keccak256(0) + 2等位置。

更多关于存储布局的内容可以参照Solidity文档

calldata布局

在Solidity中,当你通过外部函数传递string类型的变量时,它会使用calldata进行编码。calldata是一个非变动、只读的存储空间,用于函数参数。对于动态类型如string,calldata编码规则如下:

  1. 函数选择器: calldata的前4个字节(0x000x03)一般是函数选择器。比如7fcaf666

  2. 偏移量: 接下来,calldata不直接开始存储字符串内容。它首先存储一个偏移量,该偏移量表示字符串数据开始的位置。对于一个单一的string参数,这个偏移量通常是0x20(32字节),因为前32字节用于表示这个偏移量自己(0x040x23)。比如0000000000000000000000000000000000000000000000000000000000000020

  3. 字符串长度: 在偏移量之后,即在calldata0x24位置,会存储字符串的长度,占32字节(0x240x43)。比如WTF的长度为3字节,则这一段为0000000000000000000000000000000000000000000000000000000000000003

  4. 字符串内容: 在字符串长度后,我们会看到实际的字符串内容。字符串会以UTF-8编码(字符的ASCII值)的方式存储,例如WTFUTF-8编码为575446。请注意,每个calldata槽位都有32字节,所以即使字符串很短,它也会被右填充到32字节。对于一个短字符串来说,它会占32字节(0x440x63)。比如WTF会编码为5754460000000000000000000000000000000000000000000000000000000000

因此,如果我们调用的函数选择器为7fcaf666且字符串为WTF,那么calldata为:

7fcaf666000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000035754460000000000000000000000000000000000000000000000000000000000

returndata布局

returndata的布局与calldata类似,不同的是没有了前4字节的函数选择器。如果我们返回的是字符串为WTF,那么returndata为:

000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000035754460000000000000000000000000000000000000000000000000000000000

在Huff中实现string类型

setString

下面我们来用Huff实现setString()函数,为了简单,我们只考虑短字符串(长度小于32字节)。它的逻辑就是:

  • calldata中读取字符串长度,偏移量为0x24
  • calldata中读取字符串内容,偏移量为0x44
/* 接口 */ #define function setString(string memory str_) nonpayable #define function getString() view returns (string memory) /* 方法 */ #define macro SET_STRING() = takes (0) returns (0) { 0x24 calldataload // [len] 0x00 sstore // [] storage: [0x00: len] 0x44 calldataload // [str] 0x00 sha3 sstore // [] storage: [0x00: len, sha3(0): str] stop }

下面我们实现getString()函数,它的逻辑就是从存储中读取值,在内存中拼接好,最后通过return返回。

#define macro GET_STRING() = takes (0) returns (0) { 0x20 0x00 mstore // [] memory: [0x00: 0x20] 0x00 sload // [len] memory: [0x00: 0x20] 0x20 mstore // [] memory: [0x00: 0x20, 0x20: len] 0x00 sha3 sload // [str] memory: [0x00: 0x20, 0x20: len] 0x40 mstore // [] memory: [0x00: 0x20, 0x20: len, 0x40: str] 0x60 0x00 return }

最后,我们在MAIN宏中通过selector判断要调用哪个函数。

#define macro MAIN() = takes (0) returns (0) { // 通过selector判断要调用哪个函数 0x00 calldataload 0xE0 shr dup1 __FUNC_SIG(setString) eq set_string jumpi dup1 __FUNC_SIG(getString) eq get_string jumpi // 如果没有匹配的函数,就revert 0x00 0x00 revert set_string: SET_STRING() get_string: GET_STRING() }

分析合约字节码

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

huffc src/12_String.huff -r

打印出的bytecode为:

5f3560e01c80637fcaf6661461001e57806389ea642f1461002c575f5ffd5b602435600202604435015f55005b60205f525f548060ff1660011c60205260ff191660405260605ff3

将这段字节码复制到evm.codes playground。首先,我们调用setString()函数。将Calldata设为0x7fcaf666000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000035754460000000000000000000000000000000000000000000000000000000000(调用setString函数,参数为字符串类型的"WTF")并点击运行。右下角的Storage被相应更改,运行成功。

接下来,我们调用getString()函数,读取存储中字符串的值。将Calldata设为0x89ea642f,并运行。可以看到,右下角的RETURN VALUE000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000035754460000000000000000000000000000000000000000000000000000000000,与预期一致,运行成功。

使用Foundry测试

我们可以使用Foundry写一个测试,用Solidity来验证咱们写的Huff合约是否真的能保存和返回Solidity的string类型变量。

测试合约:

// SPDX-License-Identifier: Unlicense pragma solidity ^0.8.15; import "foundry-huff/HuffDeployer.sol"; import "forge-std/Test.sol"; import "forge-std/console.sol"; contract StringTest is Test { /// @dev Address of the I12_String contract. I12_String public i12_String; /// @dev Setup the testing environment. function setUp() public { i12_String = I12_String(HuffDeployer.deploy("12_String")); } /// @dev Ensure that you can set and get the value. function testSetAndGetString() public { string memory str_ = "WTF"; i12_String.setString(str_); string memory str_get = i12_String.getString(); console.log(str_get); console.log(str_); assertEq(str_, i12_String.getString()); } } interface I12_String { function getString() external view returns (string memory); function setString(string memory) external; }

在命令行输入中输入forge test运行测试合约,可以看到测试通过!

总结

这一讲,我们介绍了如何在Huff中写入并读取string类型,并在evm.codes上成功运行了合约。


发布者: 作者: 转发
评论区 (0)
U