从foundry工程化的角度详细解读Openzeppelin中的Strings库及对应测试。
[openzeppelin]:v4.8.3,[forge-std]:v1.5.6
Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/utils/Strings.sol
在memory中创建一个数组,前32个字节存放的是数组长度,从第33个字节开始才按照字节顺序紧凑地存储数组内字节内容。
看一个测试:
contract StringsTest is Test {
function test_StringMemoryLayout() external {
string memory str = "ab";
bytes32 first32BytesInMemory;
uint8 firstByteForStringData;
uint8 secondByteForStringData;
uint8 thirdByteForStringData;
assembly{
first32BytesInMemory := mload(str)
// 取str的第33个字节内容
firstByteForStringData := byte(0, mload(add(str, 0x20)))
// 取str的第34个字节内容
secondByteForStringData := byte(1, mload(add(str, 0x20)))
// 取str的第35个字节内容
thirdByteForStringData := byte(2, mload(add(str, 0x20)))
}
// 前32字节存放string的字节长度
assertEq(bytes32(uint(2)), first32BytesInMemory);
// "a"
assertEq(97, firstByteForStringData);
// "b"
assertEq(98, secondByteForStringData);
// 第3个字节没有内容
assertEq(0, thirdByteForStringData);
}
}
封装Strings library成为一个可调用合约:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/utils/Strings.sol";
contract MockStrings {
using Strings for uint;
using Strings for address;
function toString(uint value) external pure returns (string memory){
return value.toString();
}
function toHexString(uint value) external pure returns (string memory){
return value.toHexString();
}
function toHexString(uint value, uint length) external pure returns (string memory){
return value.toHexString(length);
}
function toHexString(address addr) external pure returns (string memory){
return addr.toHexString();
}
}
全部foundry测试合约:
将一个uint类型的值转成对应十进制表示形式的字符串,即输入1024,输出"1024"。
library Strings {
// 16进制字符表
bytes16 private constant _SYMBOLS = "0123456789abcdef";
function toString(uint256 value) internal pure returns (string memory) {
// 取消solidity 0.8中对数学运算的溢出检查
unchecked {
// 使用Math.log10(),计算value对10取对数。由于结果是向下取整,所以value的十进制长度为:对10取对数结果+1
uint256 length = Math.log10(value) + 1;
// 分配内存空间,长度为value十进制表达值的字节长度。即,1024的十进制表达值的字节长度length为4
string memory buffer = new string(length);
// 定义指针,用于向内存中的string结果赋值
uint256 ptr;
assembly {
// buffer为内存中的string,内存中前32字节存放string的数据长度,后面length个字节存放value百位、十位、个位等位置上的数字对应的十进制的数组表示字符,即
// value: 1024
// buffer为 0x0000000000000000000000000000000000000000000000000000000000000004 | 31303234
// (字节长度)4 '1''0''2''4'
// ptr直接指向buffer的尾部
ptr := add(buffer, add(32, length))
}
// 开始循环
while (true) {
// ptr自递减
ptr--;
assembly {
// 当前的value对10取模,获取对应字符表中的映射字节并写入内存中。
// 第一次循环取到的是value个位上的数字,第二次循环取到的是value十位上的数字
// byte(mod(value, 10), _SYMBOLS)为从_SYMBOLS中获取到偏移量为mod(value, 10)的byte,即从16进制字符表中获取到value某位上的数字对应的十进制表达的ASCII字符。
// 注:由于前面用value对10取模,而不是16,所以mod(value, 10)永远都是小于10,即永远都只能从16进制字符表中获取前1~10字节的偏移量的值,即'0'-'9'。
// mstore8(ptr, byte(mod(value, 10), _SYMBOLS)):将得到映射后的字符倒叙写进内存buffer中。
mstore8(ptr, byte(mod(value, 10), _SYMBOLS))
// 注: 由于循环中对10取模依次是取到个位、十位、百位等等的数字,所以ptr指针从string尾部向前递减。
}
// value自除以10。下次循环时,针对的目标数字就是value中前一位的数字。
value /= 10;
// 如果value等于0,表示已经处理完了value的最高位,跳出循环。
if (value == 0) break;
}
// 返回内存中存储结果的字符串buffer
return buffer;
}
}
}
foundry代码验证
contract StringsTest is Test {
MockStrings testing = new MockStrings();
function test_ToString() external {
assertEq(testing.toString(type(uint).min), "0");
assertEq(testing.toString(1), "1");
assertEq(testing.toString(23), "23");
assertEq(testing.toString(456), "456");
assertEq(testing.toString(7890), "7890");
assertEq(testing.toString(type(uint).max), "115792089237316195423570985008687907853269984665640564039457584007913129639935");
}
}
将一个uint类型的值转成对应16进制表示形式的字符串。参数length为value的ASCII字节数。比如:255的length为1,256的length为2。
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
// 在内存中为16进制编码结果分配空间。由于16进制编码是用两个字节表示原本ASCII的一个字节,编码后长度为原来字节数的2倍。又因为16进制必须以"0x"开头(额外的两个字节),所以存储16进制编码结果的字节数组长度为 2*length+2。
bytes memory buffer = new bytes(2 * length + 2);
// 16进制表达以"0x"开头
buffer[0] = "0";
buffer[1] = "x";
// 从索引2开始为16进制编码内容。顺序从编码结果的尾部向前递减,因为是从value的低位开始向高位迭代
for (uint256 i = 2 * length + 1; i > 1; --i) {
// value与0xf做与运算,得到ASCII低4位的数值。该值正好为ASCII低4位值对应16进制编码字符在_SYMBOLS中的索引。取到ASCII低4位值对应16进制编码字符并存入buffer中
buffer[i] = _SYMBOLS[value & 0xf];
// value整体右移4位
value >>= 4;
}
// 最后要求value为0,即所有的有效数据都被16进制编码。如果剩余的value不为0,表示仍有value数据未被16进制编码,说明传入的length参数过小
require(value == 0, "Strings: hex length insufficient");
// 以字符串的形式返回16进制编码码文
return string(buffer);
}
foundry代码验证
contract StringsTest is Test {
MockStrings testing = new MockStrings();
function test_ToHexString_WithLength() external {
assertEq(testing.toHexString(type(uint).min, 1), "0x00");
assertEq(testing.toHexString(255, 1), "0xff");
assertEq(testing.toHexString(256, 2), "0x0100");
assertEq(testing.toHexString(16777215, 3), "0xffffff");
assertEq(testing.toHexString(16777216, 4), "0x01000000");
assertEq(testing.toHexString(type(uint).max, 32), "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
// revert for insufficient length
vm.expectRevert("Strings: hex length insufficient");
testing.toHexString(type(uint).max, 32 - 1);
}
}
将一个uint类型的值转成对应16进制表示形式的字符串。
ps: 实际就是对toHexString(uint256 value, uint256 length)的一个封装。这里不需要输入length,而是由value的相关计算得出。
function toHexString(uint256 value) internal pure returns (string memory) {
// 取消solidity 0.8中对数学运算的溢出检查
unchecked {
// value对256取对数,向下取整。实际上就是在求value的ASCII字节数。由于是向下取整,加1得到value的真实ASCII字节数
return toHexString(value, Math.log256(value) + 1);
}
}
foundry代码验证
contract StringsTest is Test {
MockStrings testing = new MockStrings();
function test_ToHexString_WithoutLength() external {
assertEq(testing.toHexString(type(uint).min), "0x00");
assertEq(testing.toHexString(255), "0xff");
assertEq(testing.toHexString(256), "0x0100");
assertEq(testing.toHexString(16777215), "0xffffff");
assertEq(testing.toHexString(16777216), "0x01000000");
assertEq(testing.toHexString(type(uint).max), "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
}
}
将一个address类型地址值转成对应的十六进制的字符串(输出均为小写字母,不满足checksum)。
library Strings {
// 一个以太坊地址的字节长度
uint8 private constant _ADDRESS_LENGTH = 20;
function toHexString(address addr) internal pure returns (string memory) {
// 以固定的地址长度(20个字节)封装库函数:toHexString(uint256)
return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);
}
}
foundry代码验证
contract StringsTest is Test {
MockStrings testing = new MockStrings();
function test_ToHexString_FromAddress() external {
assertEq(testing.toHexString(
address(0)),
"0x0000000000000000000000000000000000000000"
);
// not checksummed
assertEq(testing.toHexString(
address(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF)),
"0xffffffffffffffffffffffffffffffffffffffff"
);
assertEq(
testing.toHexString(address(2 ** 160 - 1)),
"0xffffffffffffffffffffffffffffffffffffffff"
);
}
}
ps:\ 本人热爱图灵,热爱中本聪,热爱V神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!
公众号名称:后现代泼痞浪漫主义奠基人
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!