assembly

SolidityにおけるInline AssemblyとStringについて

Inline AssemblyとStringについて、コード書く時の参考をイメージしてまとめました。

https://github.com/eggdragons/how-to-use-foundry

Inline AssemblyとString

Inline AssemblyにおいてStringを上手に扱うと大幅にガス効率が上がります。

一方で、Stringは非常に独特な挙動を示すため、非常にわかりにくくなっています。

文字の長さによって挙動が変わる

stringは、31バイト以上と以下とで挙動が大きく変わります。

31バイト以下の場合

31バイト以下の場合、1スロットしか使用しません。

1スロットの構造は次の通りとなります。

1スロット目:31バイト(stringの情報)+1バイト(stringの長さ×2)

最後に、stringの長さが格納されるために、31バイト以下となっているんですね。

32バイト以上の場合

32バイト以上の場合、大きく変わります。

1スロット目:31バイト(空白)+1バイト(stringの長さ×2)

2スロット目以降:stringの情報

string情報に着目するとややこしいですが、stringの長さに着目するとどちらも1スロット目の最後の1バイトに長さ情報が含まれていることがわかります。

このことから、1スロット目の最後の1バイトの長さを取得し、その長さに応じてケースわけをして値を取得していきます。

メモリーとストレージ

少し話が変わりますが、メモリーとストレージでアクセスの仕方が変わります。

メモリーにアクセスする場合

mload(value)→長さ

mload(value+32)→文字

ストレージにアクセスする場合

value.slotは、valueのスロット位置

さらにややこしいのが、32バイト以上の場合

sload(keccak256(value.slot))→長さ

sload(keccak256(value.slot)+1)→一つ目の情報

無茶苦茶複雑に絡み合っていきます。

この辺りは、remixのデバッグでチェックすると、スロット位置が表示できるので、非常によくわかると思います

実際のコード

実際のコードを見ていきましょう!

https://github.com/eggdragons/how-to-use-foundry/blob/main/src/utils/HandleStringAssembly.sol

Inline Assemblyを使わない場合

    function setStringStorage(string memory str) public {
        stringStorage = str;
    }

    function getStringStorage() public view returns (string memory) {
        return stringStorage;
    }

無茶苦茶シンプルですね!何も説明することはありません。

Inline Assemblyを使ってStringをSetする

    function setStringStorageAssembly(string memory str) public {
        assembly {
            let slot := stringStorage.slot
            let len := mload(str)

            switch lt(len, 32)
            // length < 32
            case 1 {
                // (value & length) set to slot
                sstore(slot, add(mload(add(str, 0x20)), mul(len, 2)))
            }
            // length >= 32
            default {
                // length info set to slot
                sstore(slot, add(mul(len, 2), 1))

                // key
                mstore(0x0, slot)
                let sc := keccak256(0x00, 0x20)

                // value set
                for {
                    let mc := add(str, 0x20)
                    let end := add(mc, len)
                } lt(mc, end) {
                    sc := add(sc, 1)
                    mc := add(mc, 0x20)
                } { sstore(sc, mload(mc)) }
            }
        }
    }

一気に複雑になりました。

長さが31バイト以下の場合、最初の31バイトに文字を最後の1バイトに文字の長さ×2を格納しています。

長さが32バイト以上の場合、1スロット目に文字の長さ×2+1を格納しています。

その後ループ処理にて、文字情報を格納していってます。

コードを見ると単純明快ですね!

Inline Assemblyを使ってStringをGetする

    function getStringStorageAssembly() public view returns (string memory str) {
        assembly {
            // free memory pointer
            str := mload(0x40)

            let slot := stringStorage.slot
            let value := sload(slot)
            let len := div(and(value, sub(mul(0x100, iszero(and(value, 1))), 1)), 2)
            let mc := add(str, 0x20)

            // set length
            mstore(str, len)

            // set value
            switch lt(len, 32)
            // length < 32
            case 1 { mstore(mc, value) }
            // length >= 32
            default {
                // key
                mstore(0x0, slot)
                let sc := keccak256(0x00, 0x20)

                for { let end := add(mc, len) } lt(mc, end) {
                    sc := add(sc, 1)
                    mc := add(mc, 0x20)
                } { mstore(mc, sload(sc)) }
            }

            mstore(0x40, and(add(add(mc, len), 31), not(31)))
        }
    }

今度は取得していきます。

こちらも考え方自体は一緒ですので、特段説明することはないのですが、ストレージから取得しているためスロットの取得の仕方が複雑なことに注意する必要があります。

GithubのリポジトリではfoundryでのテストコードとMappingの参考コードも含めていますので、併せてご確認頂ければと思います。

私も説明がうまいことできないのですが、コードを読んでもらえばよく理解できると思います。

-assembly