foundry

foundryの基本的なテストについて

foundryは、機能が豊富だし、Fuzzテストや差分テストなどができます。

差分テストは、コントラクト外のプログラムとの差分をチェックすることができるので、既存のプログラムをコントラクトに移行する際には利用していきたいテストになっていると思っています!

ここでは、特にお勧めしたいFuzzテストとStorageの確認について、簡易的なテストを用いて説明していきたいと思います。

ここで使っているテストは、Githubで共有してます

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

Fuzzテストについて

Fuzz.t.solが対象のテストになります。

簡単に言えば、引数に値を代入するのではなく、変数を代入してランダムな値でテストするイメージです。

foundry使い出すと、ずっとこればかりになるので、すぐに使うことになると思います。

非常にわかりやすい例だったので、foundryの公式にあるコードをそのまま持ってきています。

まずは、testWithdraw1を実行してみてください。

forge test --match-contract Fuzz --match-test testWithdraw1

エラーが表示されたと思います。

引数に、uint256 amountが採用されていますが、コントラクト側の制限がuint96までのため、エラーが発生しています。

そこで、引数にuint96 amountを採用したtestWithdraw2を実行してみましょう!

forge test --match-contract Fuzz --match-test testWithdraw2

すると、テストが合格になったと思います!

uint256は変換不要なのでガス効率が上がりますがが、使い方によっては不適切な可能性があります

今主流のNFTプロジェクトではあまり大きな問題にはならないですが、DeFi関係は気をつけたい問題ですね!

Slotについて

Slot.t.solが対象のテストになります。

ここでは、テスト自体はないのですが、StorageやSlotについて理解できるかと思います。

簡易的な内容になっているので、Slot大丈夫な人は飛ばしてください!

コントラクトに記載するデータの並びによって使用するStorageのSlot数がかわります。

Slot数が増えれば増えるほどガス代が高くなります。

難しい事はさておき、実感してみましょう!まずは、下記コマンドを実行してみてください。

forge inspect SlotContract1 storage-layout --pretty

すると、

| Name | Type    | Slot | Offset | Bytes | Contract                              |
|------|---------|------|--------|-------|---------------------------------------|
| a    | uint256 | 0    | 0      | 32    | foundry-test/Slot.t.sol:SlotContract1 |
| b    | uint256 | 1    | 0      | 32    | foundry-test/Slot.t.sol:SlotContract1 |
| c    | uint256 | 2    | 0      | 32    | foundry-test/Slot.t.sol:SlotContract1 |

これを見ると、aはSlot0に、bはSlot1に、cはSlot2に格納されているのがわかります。

同じように、今度は、SlotContract2を見てみましょう!

forge inspect SlotContract2 storage-layout --pretty
| Name | Type    | Slot | Offset | Bytes | Contract                              |
|------|---------|------|--------|-------|---------------------------------------|
| a    | uint64  | 0    | 0      | 8     | foundry-test/Slot.t.sol:SlotContract2 |
| b    | uint64  | 0    | 8      | 8     | foundry-test/Slot.t.sol:SlotContract2 |
| c    | uint256 | 1    | 0      | 32    | foundry-test/Slot.t.sol:SlotContract2 |

すると、今度は、aとbがSlot0に、cがSlot1に格納されています。

Offset位置やBytes数も表示されておりわかりやすいですよね!

最後に、SlotContract3を見てみましょう!

forge inspect SlotContract3 storage-layout --pretty
| Name | Type    | Slot | Offset | Bytes | Contract                              |
|------|---------|------|--------|-------|---------------------------------------|
| a    | uint64  | 0    | 0      | 8     | foundry-test/Slot.t.sol:SlotContract3 |
| b    | uint256 | 1    | 0      | 32    | foundry-test/Slot.t.sol:SlotContract3 |
| c    | uint64  | 2    | 0      | 8     | foundry-test/Slot.t.sol:SlotContract3 |

コードの順番で、32bytes単位でSlotに格納されていくことがわかったかと思います!

Storage関係

Store.t.solが対象のテストになります。

  • testGetStore:Storageからデータを取得するテスト
  • testSetAndGetStore:Storageにデータを書き込んでデータを取得するテスト
  • testCheckStore:balanceOfをスロットから取得して値が一致するか確認するテスト

testGetStore / testSetAndGetStore

まずは、下記コマンドで対象のテストのみを実行してみましょう!

forge test --match-contract Store

すると、エラーが発生すると思います。これは、あえて参照するStorageの場所を変更しています。

では、Storage情報を取得してみましょう!下記コマンドを実行してください。

forge inspect StoreContract storage-layout --pretty

すると、下記のようなstorage情報が取得できると思います。

| Name      | Type                        | Slot | Offset | Bytes | Contract                               |
|-----------|-----------------------------|------|--------|-------|----------------------------------------|
| number1   | uint256                     | 0    | 0      | 32    | foundry-test/Store.t.sol:StoreContract |
| number2   | uint256                     | 1    | 0      | 32    | foundry-test/Store.t.sol:StoreContract |
| number3   | uint256                     | 2    | 0      | 32    | foundry-test/Store.t.sol:StoreContract |
| balanceOf | mapping(address => uint256) | 3    | 0      | 32    | foundry-test/Store.t.sol:StoreContract |

このStorage情報をもとに書き換えていきましょう!

    function testGetStore1() public {
        bytes32 data = vm.load(address(storeContract), bytes32(uint256(10)));
        assertEq(uint256(data), 123);
    }

このテストのvm.loadの第二引数に注目してください。

このコードだとSlot10を参照してくださいってなっていますので、ここを参照したいSlot番号(今回のケースでは0)に修正します。

    function testGetStore1() public {
        bytes32 data = vm.load(address(storeContract), bytes32(uint256(0)));
        assertEq(uint256(data), 123);
    }

これで、再度テストを実行してみてください。単体テストを行う場合は、下記のように実行します!

forge test --match-contract Store --match-test testGetStore1

これで合格できたかと思います!残りも修正して実行してみてくださいね!

testCheckStore

    function testCheckStore(address addr, uint256 amount) public {
        storeContract.balanceUp(addr, amount);
        bytes32 balanceSlot = keccak256(abi.encodePacked(uint256(uint160(addr)), uint256(40)));
        uint256 balance = uint256(vm.load(address(storeContract), balanceSlot));

        assertEq(balance, storeContract.balanceOf(addr));
    }

Mappingのスロット位置をmapとし、keyをkeyとすると、valueはkeccak256(key,map)の位置に保存されていることがわかります。

テストコードをよく見てもらうと、スロットがよく理解できるようになると思います。

-foundry