Michael.W基于Foundry精读Openzeppelin第6期——Strings.sol

  • Michael.W
  • 更新于 2023-07-10 09:05
  • 阅读 1539

从foundry工程化的角度详细解读Openzeppelin中的Strings库及对应测试。

0. 版本

[openzeppelin]:v4.8.3,[forge-std]:v1.5.6

0.1 Strings.sol

Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/utils/Strings.sol

1. 补充:关于memory的string的layout

在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);
    }
}

2. 目标合约

封装Strings library成为一个可调用合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/src/utils/MockStrings.sol

// 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测试合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/utils/Strings.t.sol

3. 代码精读

3.1 toString(uint256)

将一个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");
    }
}

3.2 toHexString(uint256,uint256)

将一个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);
    }
}

3.3 toHexString(uint256)

将一个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");
    }
}

3.4 toHexString(address)

将一个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神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!

1.jpeg

公众号名称:后现代泼痞浪漫主义奠基人

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Michael.W
Michael.W
0x93E7...0000
狂热的区块链爱好者