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)
の位置に保存されていることがわかります。
テストコードをよく見てもらうと、スロットがよく理解できるようになると思います。