assembly

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

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

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

Inline Assemblyを使ってStorageへのアクセスについて

先ほどのリポジトリの中のsrc/storage/StorageAssmebly.solが対象になります。

test/storage/StorageAssembly.t.solが上記のテストになっています。

このコントラクトでは、slot0にstorageAが、slot1にstorageBが、slot2にstorageCが格納されています。

    uint256 public storageA = 1; // slot0
    uint256 public storageB = 2; // slot1
    uint256 public storageC = 3; // slot2

forge inspect StorageAssembly storage-layout --prettyをターミナルで入力してもらうと、このコントラクトのストレージレイアウトを確認できるので、やってみてくださいね!

Assemblyを使用してstorageへアクセスするには、slot番号を使ってアクセスします。

    function getSlotStorageA() public pure returns (uint256 _slot) {
        assembly {
            _slot := storageA.slot
        }
    }

このような方法で、slot番号を取得します。

storageへの読み書きは、次のような方法でやります。

    function getStorageValue(uint256 slot) public view returns (uint256 value) {
        assembly {
            value := sload(slot)
        }
    }

    function setStorageValue(uint256 slot, uint256 value) public {
        assembly {
            sstore(slot, value)
        }
    }

foundryでは、次のような方法でstorageにアクセスすることができます。

    // find slot
    function testGetSlotForgeStdStorage() public {
        uint256 slot = stdstore.target(address(storageAssembly)).sig("storageA()").find();
        assertEq(slot, storageAssembly.getSlotStorageA());
    }

    // read value
    function testReadStorageValueForgeStdStorage() public {
        uint256 value = stdstore.target(address(storageAssembly)).sig("storageA()").read_uint();
        assertEq(value, storageAssembly.storageA());
    }

    // wtite value
    function testWriteStorageValueForgeStdStorage(uint256 value) public {
        stdstore.target(address(storageAssembly)).sig("storageA()").checked_write(value);
        assertEq(value, storageAssembly.storageA());
    }

Inline Assemblyを使ってPackingされたStorageへのアクセスについて

ここでは、src/storage/PackingStorageAssembly.solが対象になります。

test/storage/PackingStorageAssembly.t.solが上記のテストになっています。

このコントラクトでは、slot0にstorageD、storageE、storageFがパッキングされて格納されています。

    uint128 public storageD = 10;
    uint64 public storageE = 20;
    uint64 public storageF = 30;

    /*
    | uint64   | uint64   | uint128  |
    | storageF | storageE | storageD |
    | 00...030 | 00...020 | 00...010 |
    */

forge inspect PackingStorageAssembly storage-layout --prettyをターミナルで入力してもらうと、このコントラクトのストレージレイアウトを確認できるので、やってみてくださいね!

Assemblyを使用してstorageへアクセスするためには、前章の通りslot番号を使ってアクセスする必要があります。

しかし、今回のケースの場合、一つのslotにstorageD、storageE、storageFが格納されています。

そのためは、下記の結果は、すべて0になります。

    function getSlotStorageD() public pure returns (uint256 _slot) {
        assembly {
            _slot := storageD.slot
        }
    }

    function getSlotStorageE() public pure returns (uint256 _slot) {
        assembly {
            _slot := storageE.slot
        }
    }

    function getSlotStorageF() public pure returns (uint256 _slot) {
        assembly {
            _slot := storageF.slot
        }
    }

そこで、各データへの読み書きを行う際は、自分でビットシフトをしたりパッキングする必要があります。

    function getStorageValuePacking(uint256 slot, uint256 key) public view returns (uint256 value) {
        assembly {
            value := sload(slot)

            switch key
            case 0 {
                let mask := sub(shl(128, 1), 1)
                value := and(value, mask)
            }
            case 1 {
                value := shr(128, value)
                let mask := sub(shl(64, 1), 1)
                value := and(value, mask)
            }
            case 2 {
                value := shr(192, value)
                let mask := sub(shl(64, 1), 1)
                value := and(value, mask)
            }
            default {}
        }
    }

なお、上記コードは分かりやすい様に書いたコードですので、実際に使用する際は、下記のようなコードを使うことが多いです。

    function getStorageValuePackingPractical(uint256 slot) public view returns (uint128 d, uint64 e, uint64 f) {
        assembly {
            let value := sload(slot)
            d := value
            e := shr(128, value)
            f := shr(192, value)
        }
    }

なお、foundryでは、まだパッキングスロットへのアクセスはサポートされていません。

Inline Assemblyを使ってMappingされたStructStorageへのアクセスについて

ここでは、src/storage/StructStorageAssembly.solが対象になります。

test/storage/StuctStorageAssembly.t.solが上記のテストになっています。

このコントラクトでは、下記の様なmappingStructStorageに各データを格納するコントラクトになっています。

    struct StructStorage {
        uint128 storageH;
        uint64 storageI;
        uint64 storageJ;
        uint64 storageK;
    }

    mapping(uint256 => StructStorage) public mappingStructStorages;

    /*
    Slot α + 0
    | uint64   | uint64   | uint128  |
    | storageJ | storageI | storageH |
    | 00...030 | 00...020 | 00...010 |

    Slot α + 1
    | uint64   |
    | storageK |
    | 00...030 |
    */

Assemblyを使用して各データへアクセスする方法は今までの内容を複合させた感じになります。

Structの中も今まで同様パッキングされており、今回のケースだとSlotA(storageJ、storageI、storageHがパッキングされたもの)とSlotB(storageK)が含まれています。

それぞれ各Slotへアクセスするためには、StructのSlotがαだとすると、SlotAはα+0、SlotBはα+1になります。

    // if you want to test index=0 you need to change the return type
    function getMappingStructStorageAssembly(uint256 key, uint256 index) public view returns (uint64 result) {
        uint128 h;
        uint64 i;
        uint64 j;
        uint64 k;

        assembly {
            mstore(0x00, key)
            mstore(0x20, mappingStructStorages.slot)
            let slot := keccak256(0, 0x40)

            let value := sload(slot)

            h := and(value, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
            i := shr(128, value)
            j := shr(192, value)

            value := sload(add(slot, 1))
            k := value

            switch index
            case 1 { result := i }
            case 2 { result := j }
            case 3 { result := k }
            default { result := 0 }
        }
    }

なお、上記コードは分かりやすい様に書いたコードですので、実際に使用する際は、下記のようなコードを使うことが多いです。

    function getMappingStructStorageAssemblyOtherSlotMethod(uint256 key)
        public
        view
        returns (uint128 h, uint64 i, uint64 j, uint64 k)
    {
        StructStorage storage mappingStructStorage = mappingStructStorages[key];

        assembly {
            let slot := mappingStructStorage.slot
            let value := sload(slot)

            h := and(value, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
            i := shr(128, value)
            j := shr(192, value)

            value := sload(add(slot, 1))
            k := value
        }
    }

なお、foundryでは、下記の様な方法でアクセスします。

    // read value Mapping
    function testReadMappingStorageValueForgeStdStorage(uint256 key, uint256 value) public {
        // init
        storageAssembly.setMappingStorage(key, value);

        uint256 read_value =
            stdstore.target(address(storageAssembly)).sig("mappingStorage(uint256)").with_key(key).read_uint();
        assertEq(read_value, storageAssembly.getMappingStorage(key));
    }

Inline Assemblyを使ってPackingされたStorageへのアクセスについて

ここでは、src/storage/PackingStorageAssembly.solが対象になります。

test/storage/PackingStorageAssembly.t.solが上記のテストになっています。

このコントラクトでは、slot0にstorageD、storageE、storageFがパッキングされて格納されています。

    uint128 public storageD = 10;
    uint64 public storageE = 20;
    uint64 public storageF = 30;

    /*
    | uint64   | uint64   | uint128  |
    | storageF | storageE | storageD |
    | 00...030 | 00...020 | 00...010 |
    */

forge inspect PackingStorageAssembly storage-layout --prettyをターミナルで入力してもらうと、このコントラクトのストレージレイアウトを確認できるので、やってみてくださいね!

Assemblyを使用してstorageへアクセスするためには、前章の通りslot番号を使ってアクセスする必要があります。

しかし、今回のケースの場合、一つのslotにstorageD、storageE、storageFが格納されています。

そのためは、下記の結果は、すべて0になります。

    function getSlotStorageD() public pure returns (uint256 _slot) {
        assembly {
            _slot := storageD.slot
        }
    }

    function getSlotStorageE() public pure returns (uint256 _slot) {
        assembly {
            _slot := storageE.slot
        }
    }

    function getSlotStorageF() public pure returns (uint256 _slot) {
        assembly {
            _slot := storageF.slot
        }
    }

そこで、各データへの読み書きを行う際は、自分でビットシフトをしたりパッキングする必要があります。

    function getStorageValuePacking(uint256 slot, uint256 key) public view returns (uint256 value) {
        assembly {
            value := sload(slot)

            switch key
            case 0 {
                let mask := sub(shl(128, 1), 1)
                value := and(value, mask)
            }
            case 1 {
                value := shr(128, value)
                let mask := sub(shl(64, 1), 1)
                value := and(value, mask)
            }
            case 2 {
                value := shr(192, value)
                let mask := sub(shl(64, 1), 1)
                value := and(value, mask)
            }
            default {}
        }
    }

なお、上記コードは分かりやすい様に書いたコードですので、実際に使用する際は、下記のようなコードを使うことが多いです。

    function getStorageValuePackingPractical(uint256 slot) public view returns (uint128 d, uint64 e, uint64 f) {
        assembly {
            let value := sload(slot)
            d := value
            e := shr(128, value)
            f := shr(192, value)
        }
    }

なお、foundryでは、まだパッキングスロットへのアクセスはサポートされていません。

一方で、パッキングされていないStructのSlotへはアクセスできます。

    struct StructStorageFoundry {
        uint256 storageX;
        uint256 storageY;
    }

    mapping(uint256 => StructStorageFoundry) public mappingStructStoragesFoundry;
    // read value Struct Mapping
    function testReadMappingStorageValueForgeStdStorage(uint256 key, uint256 valueX, uint256 valueY) public {
        // init
        storageAssembly.setMappingStructStorageFoundry(key, valueX, valueY);

        // X depth 0
        uint256 slotX = stdstore.target(address(storageAssembly)).sig(
            storageAssembly.mappingStructStoragesFoundry.selector
        ).with_key(key).depth(0).find();

        uint256 x = uint256(vm.load(address(storageAssembly), bytes32(uint256(slotX))));

        // Y depth 1
        uint256 y = stdstore.target(address(storageAssembly)).sig(storageAssembly.mappingStructStoragesFoundry.selector)
            .with_key(key).depth(1).read_uint();

        assertEq(x, valueX);
        assertEq(y, valueY);
    }

今回は、Inline Assemblyを使用してさまざまなStorageへのアクセスをまとめてみました

-assembly