Fracton エンジニアブログ

Fractonの技術ブログ

Solidity開発ツール、Foundryの紹介

こんにちは、Fractonのリサーチエンジニアの池田です。

Solidityのフレームワークの一つであるFoundryの使い方を説明します。FoundryはCrypto VC大手のParadigmが開発している、Solidity開発フレームワークです。

テストコードをJavaScriptやTypeScriptではなくSolidityで記述するため、テストの記述が比較的楽であるという特徴があります。デバッグもやりやすく、今後のSolidity開発で役立つこと間違いなしのツールとして注目を集めています。

Foundryの使い方

公式のDocsはこちらです。 公式のDocsが非常に丁寧に書かれているため、これを見れば使い方はわかります。

book.getfoundry.sh

またFoundryのtemplateとしてこちらのレポジトリーも参考になります。

github.com

openzeppelinのERC20をインポートして、テスト、デプロイをしており、Foundryの基本的な使い方が学べます。

環境構築

新規にFoundryプロジェクトを作るときは、以下のようにinitします。この場合は、hello_foundryディレクトリにプロジェクトが作成されます。

forge init hello_foundry

既存のFoundryプロジェクトを使うときは、git cloneしたあと、packageを以下のコマンドでインストールすれば使えるようになります。

forge install

forge installで新規にpackageをインストールしたい場合は、以下のようにします。これでOpenzeppelinのpackageが使えるようになります。なお、FoundryではパッケージのインストールにGIt Modulesが利用されています。

forge install OpenZeppelin/openzeppelin-contracts

テスト

テストコードはSolidityで書かれます。Hardhatよりもテストを高速に実行でき、体感的にもかなりサクサクにテストが実行できて快適です。

以下、公式Docsのサンプルを使って、使い方を見てみます。

pragma solidity 0.8.10;

import "forge-std/Test.sol";

error Unauthorized();

contract OwnerUpOnly {
    address public immutable owner;
    uint256 public count;

    constructor() {
        owner = msg.sender;
    }

    function increment() external {
        if (msg.sender != owner) {
            revert Unauthorized();
        }
        count++;
    }
}

contract OwnerUpOnlyTest is Test {
    OwnerUpOnly upOnly;

    function setUp() public {
        upOnly = new OwnerUpOnly();
    }

    function testIncrementAsOwner() public {
        assertEq(upOnly.count(), 0);
        upOnly.increment();
        assertEq(upOnly.count(), 1);
    }
}

import "forge-std/Test.sol";というFoundry標準のTestモジュールを使用します。このTestの中に、assertEq, expectEmit, expectRevertなどテストに必要な機能が全て入っています。上記の例では、OwnerUpOnlyがテストしたいコントラクトで、OwnerUpOnlyTestがテストコントラクトです。upOnly.increment();によって、count()が1になっていることをテストしています。

testコマンドは以下です。テストを実行すると、functionの名前がtestから始まるものがテストとして実行されます。今回の例では、testIncrementAsOwner()です。

forge test

実行すると以下の結果が表示されます。

$ forge test
Compiling 7 files with 0.8.10
Compiler run successful

Running 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] testIncrementAsOwner() (gas: 29162)
Test result: ok. 1 passed; 0 failed; finished in 723.90µs

またテストのログ機能も非常に便利です。v flagオプションにより、ログを出力することができます。vの数で、ログをどの深さまで出力するかを選べます。5個(-vvvvv)が最大で、これを実行するとテストで実行されたすべてのメソッドのトレースを出力できます。

forge test -vvvvv

上記の例でやってみると、count()、increment()などの、途中の実行結果が表示されるようになり、デバッグが非常にやりやすくなります。

  [24661] OwnerUpOnlyTest::testIncrementAsOwner()
    ├─ [2262] OwnerUpOnly::count()
    │   └─ ← 0
    ├─ [20398] OwnerUpOnly::increment()
    │   └─ ← ()
    ├─ [262] OwnerUpOnly::count()
    │   └─ ← 1
    └─ ← ()

Cast コマンド

castコマンドを使って、cliからethereum rpcにリクエストし、スマートコントラクトをcallしたり、transactionを送信したりできます。

以下のようにname()メソッドをDai Stablecoinに対してcallすると、name="Dai Stablecoin"が返ってきます。

$  cast call 0x6b175474e89094c44da98b954eedeac495271d0f "name()(string)" --rpc-url https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf
Dai Stablecoin

totalSupply()を実行すれば、totalSupply=64255...が返ります。

$  cast call 0x6b175474e89094c44da98b954eedeac495271d0f "totalSupply()(uint256)" --rpc-url https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf
6425577805499084133345931551

sendを使うとtransactionを送信できます。rinkebyのUSDCに対して、approve()を実行してみます。(address, uint256)=(0xdE62..., 1)をparamsとして実行してみます。

cast send 0xeb8f08a975Ab53E34D8a0330E0D34de942C95926 "approve(address, uint256)" 0xdE62195B5DF46D95D5349944E66E39d7C2a591f7 1  --rpc-url https://eth-rinkeby.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf --private-key=$PRIVATE_KEY

実行するとtransaction receiptが返ってきます。

blockHash               0x8cbb654e7f849c27d0cc6b24e73c81d0216f6afedde0024e8f808e721333c37e
blockNumber             11002969
contractAddress         
cumulativeGasUsed       26211
effectiveGasPrice       3000000008
gasUsed                 26211
logs                    [{"address":"0xeb8f08a975ab53e34d8a0330e0d34de942c95926","topics":["0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925","0x000000000000000000000000de62195b5df46d95d5349944e66e39d7c2a591f7","0x000000000000000000000000de62195b5df46d95d5349944e66e39d7c2a591f7"],"data":"0x0000000000000000000000000000000000000000000000000000000000000001","blockHash":"0x8cbb654e7f849c27d0cc6b24e73c81d0216f6afedde0024e8f808e721333c37e","blockNumber":"0xa7e459","transactionHash":"0xddbf59b05b49b156a7272e7a08d87b8426aa0004a86400e8a8817f2f255916e4","transactionIndex":"0x0","logIndex":"0x0","removed":false}]
logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000002000000000010000000000000000000001000000000000000000000010000000000000000000000000000000000000000000000000000000000040
root                    
status                  1
transactionHash         0xddbf59b05b49b156a7272e7a08d87b8426aa0004a86400e8a8817f2f255916e4
transactionIndex        0
type                    2
cast send 0xeb8f08a975Ab53E34D8a0330E0D34de942C95926 "approve(address, uint256)" 0xdE62195B5DF46D95D5349944E66E39d7C2a591f7 1  --rpc-url https://eth-rinkeby.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf --private-key=$PRIVATE_KEY

Anvil コマンド

Anvilコマンドはローカルノードを立ち上げるためのコマンドです。anvilcliに入力すると、以下のようにノードが立ち上がります。GanacheやHardhatノードよりもかなり高速に動作するようです。

$ anvil

                             _   _
                            (_) | |
      __ _   _ __   __   __  _  | |
     / _` | | '_ \  \ \ / / | | | |
    | (_| | | | | |  \ V /  | | | |
     \__,_| |_| |_|   \_/   |_| |_|

    0.1.0 (8216a10 2022-06-01T00:04:28.362698536Z)
    https://github.com/foundry-rs/foundry

Available Accounts
==================
(0) 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
(1) 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 (10000 ETH)
(2) 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc (10000 ETH)
(3) 0x90f79bf6eb2c4f870365e785982e1f101e93b906 (10000 ETH)
(4) 0x15d34aaf54267db7d7c367839aaf71a00a2c6a65 (10000 ETH)
(5) 0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc (10000 ETH)
(6) 0x976ea74026e726554db657fa54763abd0c3a0aa9 (10000 ETH)
(7) 0x14dc79964da2c08b23698b3d3cc7ca32193d9955 (10000 ETH)
(8) 0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f (10000 ETH)
(9) 0xa0ee7a142d267c1f36714e4a8f75612f20a79720 (10000 ETH)

Private Keys
==================
(0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
(1) 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
(2) 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
(3) 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
(4) 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a
(5) 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba
(6) 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
(7) 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356
(8) 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97
(9) 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6

Wallet
==================
Mnemonic:          test test test test test test test test test test test junk
Derivation path:   m/44'/60'/0'/0/

Base Fee
==================
1000000000

Gas Price
==================
20000000000

Gas Limit
==================
30000000

Listening on 127.0.0.1:8545

anvilはローカルノードだけではなく、実際に動いているネットワークをフォークしてローカルで実行できます。fork-urlオプションでmainnetを指定して実行します。

anvil --fork-url https://mainnet.infura.io/v3/$INFURA_KEY

環境変数をセットし、ALICE, 適当なUSERの残高を調べてみます。

export DAI=0x6b175474e89094c44da98b954eedeac495271d0f
export USER=0xad0135af20fa82e106607257143d0060a7eb5cbf

USERの残高を呼び出すと、mainnet forkしたローカルネットワークにcallするので、mainetの残高を見ることができます。

$ cast call $DAI \
  "balanceOf(address)(uint256)" \
  $USER

71686045944718512103110072

まとめ

Foundryを使うとSolidityでテストコードがかけ、高速にテストが実行できます。cast, anvilなどのツールも充実しているので、今後Solidity開発になくてはならないツールとなると思います。

Foundryを使ったDifferential testingについて

こんにちは、Fractonのリサーチエンジニアの池田です。

今回はFoundryのDifferential testingについて説明します。

こちらが公式のDocsです。

book.getfoundry.sh

背景

Differential testingとは、同様のアルゴリズムや関数に関する複数の異なる実装の実行結果が一致していることを確認するテスト手法です。FoundryではSolidityで実装された関数の出力を、PythonJavaScriptで書いた別の実装の出力と一致しているか確認することができます。

たとえば複雑な数式を使うF(x)という関数をSolidityで実装したいとします。このときSolidityでF(x)を実装したものをf1(x)、PythonでF(x)を実装したものをf2(x)とします。さまざまなxに対し、f1(x) == f2(x)を確認することで、Solidityでの計算結果を外部の計算結果と突き合わせ検算することができます。このようにさまざまなリソースを参照することで複雑な数式の検証も効率的に行うことができます。

FFIについて

Differential testingを実行するため、 FFI*1機能を使います。 FoundryではFFIを使うことで、テストコードで任意のシェルコマンドを実行できます。

例えば以下のテストのようにcmdsを定義すると、cats address.txtというシェルコマンドが、vm.ffi(cmds)で実行され、resultにaddress.txtの中身が代入されます。

import "forge-std/Test.sol";

contract TestContract is Test {

    function testMyFFI () public {
        string[] memory cmds = new string[](2);
        cmds[0] = "cat";
        cmds[1] = "address.txt"; // assume contains abi-encoded address.
        bytes memory result = vm.ffi(cmds);
        address loadedAddress = abi.decode(result, (address));
        // Do something with the address
        // ...
    }
}

上記の例では、abi.encodeされたアドレスをaddress.txtに書いておけば、FFIで読み込んだあとabi.decodeすることでloadedAddressとして使うことができます。

Gradual Dutch Auctionの例

公式DocsでDifferential testingのサンプルとしてGradual Dutch Auctionが紹介されています。

github.com

この例では、価格の数学的モデルとして、以下の式をコントラクトで実装しています。 Paradigm参考文献

このような複雑な数式のテストを手作業で書くのは非常に大変ですし、思いがけないバグがどこかにありそれに気付けない可能性があります。そんな時に、Differential testingを使うことにより外部ソースとの検証を効率的に行うことができます。

以下ソースコードを見てみます。 src/test/DiscreteGDA.t.solに、ffiを使ったDifferential testingが使われています。inputsが実行されるシェルコマンドで、analysis/compute_price.pyというPythonスクリプトを実行し、外部から計算結果を呼び出しています。

//call out to python script for price computation
function calculatePrice(
    int256 _initialPrice,
    int256 _scaleFactor,
    int256 _decayConstant,
    uint256 _numTotalPurchases,
    uint256 _timeSinceStart,
    uint256 _quantity
) private returns (uint256) {
    string[] memory inputs = new string[](15);
    inputs[0] = "python3";
    inputs[1] = "analysis/compute_price.py";
    inputs[2] = "exp_discrete";
    inputs[3] = "--initial_price";
    inputs[4] = uint256(_initialPrice).toString();
    inputs[5] = "--scale_factor";
    inputs[6] = uint256(_scaleFactor).toString();
    inputs[7] = "--decay_constant";
    inputs[8] = uint256(_decayConstant).toString();
    inputs[9] = "--num_total_purchases";
    inputs[10] = _numTotalPurchases.toString();
    inputs[11] = "--time_since_start";
    inputs[12] = _timeSinceStart.toString();
    inputs[13] = "--quantity";
    inputs[14] = _quantity.toString();
    bytes memory res = vm.ffi(inputs);
    uint256 price = abi.decode(res, (uint256));
    return price;
}

最終的にはcheckPriceWithParametersという部分で、Solidityの結果と、Pythonの結果が0.1%以内の精度で一致していることを確認しています。

//equal within 0.1%
utils.assertApproxEqual(actualPrice, expectedPrice, 1);

このように複雑な数式のExpected Valueとして、外部スクリプトの結果を利用することができ非常に便利です。また、fuzz testingを組み合わせることにより、さまざまな入力xについてf1(x) == f2(x)をテストすることができ、テストを効率的に行うことができます。

まとめ

Differentialテストを用いることで、手動ではテストを書くのが難しい複雑なアルゴリズムや関数も効率的にテストすることができる。

*1:おそらくForeign function interfaceの略。あるプログラミング言語から別の言語で書かれた関数などを利用する機能のことです。

HardhatとFoundryの使い分けについての雑記

Fracton CTOの赤澤です。Solidityでスマートコントラクトを開発する際に何かのツールを使うことがほとんどだと思います。そんな中でも特に人気なものといえばHardhatでしょう。また、最近人気急上昇なツールにFoundryもあります。弊社でも、この二つを基本的に使っていますが、どっちもどっちなところも多いのが事実です。両方の特徴を考慮しつつ、弊社内での使い分けを考える機会があったので、今回はそれを綴ってみました。

Hardhat

HardhatはNomic Foundationにより開発されているオープンソースであり、世界的に利用者が多いツールです。コンパイルやデプロイ、テストなど一通りの開発工程がこれ一本で完結する優れものです。しかも、プラグインを導入することで、テストカバレッジを測ったり、ガスの計算ができたりと状況に応じてかなり応用を効かせられます。

Node.jsがあればコマンド一発で導入でき、TypeScriptで開発することも可能です。

Foundry

FoundryはクリプトVCであるParadigmが開発したオープンソースです。最大の特徴はなんといっても処理の速さにあります。Rustで開発されておりパフォーマンスが高く、公式サイトにはHardhatで15.244秒掛かっていたところがFoundryだと9.449秒で完了したと書かれています。

また、テストコードをSolidityで書くことができ、型などについて頭を切り替える必要も特にありません。また、Fuzz testingやDifferential testingといったより高度なテストも簡単に実行できるので、セキュリティが求められるスマートコントラクト開発の大きな力になってくれます。

かく言う私もFoundryに感動して最近使う頻度が多くなっています、笑

hardhatにインスパイアされている使い勝手ということもあり愛好家が増えてきています。

結局どっち?

こうなってくると実際の開発現場でどっちを使うかという問題が持ち上がります。実はこの点について考えるキッカケが、弊社が実施しているインキュベーションプログラムで開催したオフィスアワー内での一コマでした。そこで開発ツールに何を使っているのかと言う話になり、HardhatやFoundryの名前が上がりましたが、その時にフロントエンドとの連携が話題になりました。

開発チームが全員それなりにSolidityが読み書きできるならまだしも、そこまで詳しいわけではない場合、Hardhatのテストコードがそのまま参考になると。

考えてみれば、本来テストコードは望ましい処理を理解する上でとても役に立つものなのでそれはそうですね。

確かに今後フロントエンドエンジニアがWeb3界隈に参入してくることを考えれば、スマートコントラクトの処理がどうなっているのか一から追いかけるのは中々に骨が折れます。テストコードが、TypeScriptなどで書かれていればそれを土台にすることはできます。

弊社の場合、フロントエンドも作る必要がある場合も多いため、これはそれなりに大事なポイントになります。当然HardhatとFoundryの両方で書けばいいという選択肢もありますが、工数が増えるのでここでは一旦ナシです。

Foundryのパフォーマンスの高さを活かしてテストをぶん回すのも捨てがたいですが、例えばFuzz testingのようなより高度で試行回数が多いようなテストはFoundryに任せてしまうのが良さそうです。

そんなこんなあり、結果的には通常の開発とテストカバレッジを100に近づける部分はHardhatで、Fuzz testingなど馬力のいるテストはFoundryでという使い方に落ち着いています。ただし、フロントエンドの開発が完全に計画に入らない場合は、Foundry一本でokという風にしています。

まとめ

今回はHardhatとFoundryの使い分けについて綴ってみました。弊社はフロントエンドの開発が計画にあるか無いかで判断するフロー考えていますが、ベストな構成なんて状況によってまちまちなのでそれぞれの環境に合わせて考えてみてください。

OpenSeaのSeaportでのFuzz testing活用事例の紹介

こんにちは、Fractonのリサーチエンジニアの池田です。

今回はFoundryのFuzz testingの利用例として、OpenSeaのSeaportを紹介します。 SeaportはOpenSeaが開発している、新しいNFTマーケットプレイスプロトコルです。 こちらのSolidityのソースコードで、FoundryのFuzz testingが本格的に使用されていたので紹介します。

github.com

Seaportでの運用

Seaportでは、HardhatによるテストとFoundryによるFuzz testingの両方を実施しています。 Fuzz testingを実施することで、開発者が想定できないようなバグを検出できる可能性があり、ソフトウェアの堅牢性を高めることができます。 設定ファイルのFoundry.tomlで、defaultの試行回数を5000と設定しており、5000回の乱数生成を行ってFuzz testingしています。

fuzz_runs = 5000

test/foundryに、テストファイルがおいてあります。 今回、その中の一つFulfillOrderTest.t.solを見てみます。

初めの方で、FuzzInputsCommonを定義しています。 これがFuzzingに使う入力で、これらの値がランダムに生成されテストが実行されます。

struct FuzzInputsCommon {
    address zone;
    uint128 id;
    bytes32 zoneHash;
    uint256 salt;
    uint128[3] paymentAmts;
    bool useConduit;
    uint120 startAmount;
    uint120 endAmount;
    uint16 warpAmount;
}

FuzzInputsCommonの後で、以下のmodifierが定義されています。vm.assume(bool)は、入力した条件が満たされなかった場合は、乱数生成をやり直す処理です。1つ目の条件は、paymentAmts[i]がすべて0より大きいという条件です。もしどれかに0が入っていた場合はpaymentAmtsの乱数を選び直します。2つ目の条件は、paymentAmts[i]の和がuint128の最大値を超えないという条件で、これにより和がオーバーフローすることを防いでいます。

modifier validateInputs(FuzzInputsCommon memory args) {
    vm.assume(
        args.paymentAmts[0] > 0 &&
        args.paymentAmts[1] > 0 &&
        args.paymentAmts[2] > 0
    );
    vm.assume(
        args.paymentAmts[0].add(args.paymentAmts[1]).add(
            args.paymentAmts[2]
        ) <= uint128(MAX_INT)
    );
    _;
}

条件に合う値が見つかるまで何回トライするかの条件は、Foundry.tomlの以下で設定されています。Seaportでは、条件に合うinputsが見つかるまで、200万回を設定しています。この値を超えても見つからなかった場合はテストが失敗します。

fuzz_max_global_rejects = 2_000_000

実際のtestを1つピックアップしてみます。 testFulfillAscendingDescendingOfferは、FuzzInputsCommonを入力としてFuzz testingしています。先程のvalidateInputs(inputs)のmodifierでinputsをvalidationしています。これ以外にも、zone, startAmount, endAmountなどの入力がvalidationされています。validation後、test()が実行され、fulfillOrderメソッドが実行できるかどうかがテストされます。

function testFulfillAscendingDescendingOffer(FuzzInputsCommon memory inputs)
    public
    validateInputs(inputs)
    onlyPayable(inputs.zone)
{
    vm.assume(inputs.startAmount > 0 && inputs.endAmount > 0);
    inputs.warpAmount %= 1000;
    test(
        this.fulfillAscendingDescendingOffer,
        Context(referenceConsideration, inputs, 0, 0, 0)
    );
    test(
        this.fulfillAscendingDescendingOffer,
        Context(consideration, inputs, 0, 0, 0)
    );
}

Seaportでは、Hardhatによるテスト、FoundryによるFuzz testingの2つのテストを実施しています。まずはHardhatによるテストで、コントラクトが意図した挙動になっているかを確認し、その後Fuzz testingにより、意図していなかったバグを洗い出すという運用になっているのかなと思います。Hardhatによるテストに加えてFuzz testingを行うことで、コントラクトの堅牢性を高める効果が期待できます。Seaportのような幅広く使われるプロトコルでは、Fuzz testingによってセキュリティを高めることが重要だと思われます。

今後スマートコントラクトにもFuzz testingが普及していくにつれ、Fuzz testingによって見つかるバグのパターンや効果的なFuzz testingの方法などの知見が広がっていくのではないでしょうか。

結論

Openseaの新プロトコルのSeaportでは、FoundryのFuzz testingを活用し、コントラクトの堅牢性を高めている。

FoundryのFuzz testingを触ってみた

こんにちは、Fractonでリサーチエンジニアをしている池田です。今回は、FoundryのFuzz testingについて説明します。Fuzz testingとは、大量のランダムデータをテストに入力し、開発者が想定しづらいバグを発見するためのテストです。

FoundryでのFuzz testingのやり方

Foundryの公式docsに使い方の説明があります。Fuzz Testing

公式docsのサンプルを使って説明します。以下のようなテストがあるとします。このテストでは、withdraw()を持ったSafeコントラクトに1ETH送って、Safeから1ETHをwithdrawできるかをテストしています。

pragma solidity 0.8.10;

import "forge-std/Test.sol";

contract Safe {
    receive() external payable {}

    function withdraw() external {
        payable(msg.sender).transfer(address(this).balance);
    }
}

contract SafeTest is Test {
    Safe safe;

    // Needed so the test contract itself can receive ether
    // when withdrawing
    receive() external payable {}

    function setUp() public {
        safe = new Safe();
    }

    function testWithdraw() public {
        payable(address(safe)).transfer(1 ether);
        uint256 preBalance = address(this).balance;
        safe.withdraw();
        uint256 postBalance = address(this).balance;
        assertEq(preBalance + 1 ether, postBalance);
    }
}

これをforge testするとpassしますが、このテストでチェックしているのは、あくまで1ETHをtransferしてwithdrawしたときのみであり、他の金額を入力したときの動作は検証されていません。

Foundryには、テストに入力するデータをランダムに生成し、想定外のデータに対しての挙動を確認するFuzz testing機能があります。

使い方は簡単で、testWithdraw()にamountというparamを入力するだけです。

contract SafeTest is Test {
    // ...

    function testWithdraw(uint256 amount) public {
        payable(address(safe)).transfer(amount);
        uint256 preBalance = address(this).balance;
        safe.withdraw();
        uint256 postBalance = address(this).balance;
        assertEq(preBalance + amount, postBalance);
    }
}

これをforge testするとamountをuint256の範囲でランダムに生成し、様々なamountに対してテストを実行してくれます。デフォルトでは、乱数生成を256回行い、256通りのシナリオをテストできます。

実はこのテストは失敗し、実行結果は以下のようになります。

$ forge test
Compiling 1 files with 0.8.10
Compiler run successful

Running 1 test for test/Safe.t.sol:SafeTest
[FAIL. Counterexample: calldata=0x215a2f200000000000000000000000000000000000000001000000000000000000000000, args=[79228162514264337593543950336]] testWithdraw(uint256) (runs: 45, μ: 19554, ~: 19554)
Test result: FAILED. 0 passed; 1 failed; finished in 23.75ms

args=[79228162514264337593543950336]] testWithdraw(uint256) (runs: 45, μ: 19554, ~: 19554)というログが出ています。runs: 45、つまり45回目の試行で、arg(amount)=79228162514264337593543950336のときに、テストが失敗したことを示しています。

失敗している理由ですが、テストの1行目のpayable(address(safe)).transfer(amount);がruns=45のargに対して、残高不足で失敗しているのが原因です。 これはFoundryのTestコントラクトの仕様ですが、デフォルトではTestの初期残高は2**96、つまりuint96の最大値に設定されています。そのためuint256の範囲でランダムに値を取ると、uint96_MAXを超えた値が入ったときに、transferが失敗します。

上記のTestの仕様を踏まえると、以下のようにtestのparamをuint96にするとテストが成功します。

function testWithdraw(uint96 amount) public {

実行結果は以下のようになります。

$ forge test
Compiling 1 files with 0.8.10
Compiler run successful

Running 1 test for test/Safe.t.sol:SafeTest
[PASS] testWithdraw(uint96) (runs: 256, μ: 13268, ~: 19654)
Test result: ok. 1 passed; 0 failed; finished in 88.82ms

runs: 256となっており、256通りの試行に対して、testがpassしていることがわかります。

デフォルトの試行回数は256回ですが、configを変えることで、試行回数は変えられます。

このようにFoundryでは、test関数にparamsを入力してあげるだけで、簡単にFuzz testingが実行できます。

結論

Foundryでは、Fuzz tesing機能が備わっており、大量のランダムデータの作成とテストを自動でやってくれる。

Fracton、技術ブログ始めたってよ。

こんにちは、FractonのCTO 赤澤です。さてこの度、Fractonとして技術ブログを立ち上げました。私を含めたFractonのチームメンバーで技術系の発信を今後進めていきます。今回は初回ということで、Fractonの概要やなぜ技術系の発信をするのかの背景を綴りたいと思います。

Fractonって何してるの?

最近よく聞かれる質問に、Fractonって何してるの?というものがあります。確かに外から見るとだいぶ分かりづらい立ち回りをしているかもしれません。FractonはWeb3/DAO領域に特化したインキュベーション事業や共同事業を通してWeb3/DAOのエコシステムに貢献することを目指しているチームです。

そのため、特にプロトコルやDAO関連を中心にスマートコントラクトや関連技術のリサーチ及び開発にも取り組んでいます。そんな諸々の活動を通して、いくらか蓄積が生まれてきているため、今回の技術ブログを通してアウトプットの場を作ったというのが経緯です。

Fractonにとってテックとは?

Fractonは設立当初からCTOというタイトルを用意し以来僭越ながら私が務めています。実は、これには明確な理由があります。設立前、Fractonの名前も決まってなかった頃、私と共に共同創業した鈴木の一言がきっかけです。

Web3やDAOの業界は技術が引っ張る業界だから、支援側にも技術が分かるエンジニアが必要だ

これは初めて鈴木から一緒に仕事をしないかと声をかけられた時から一貫して言われていたことであり、個人的にも強く共感できる内容だったので、身内ではあるものの鋭いなと思ってます。

例えば、ParadigmというクリプトVCには多くのリサーチエンジニアが在籍しており、当然のようにCTOがいます。Outlier Venturesというアクセラレーションプログラムを提供している企業にも、同じくCTOがおり技術面の責任を負っています。

Web3/DAOという分野に少しでも足を踏み入れた人であれば痛感しますが、この分野はあらゆる領域が交錯する総力戦を必要とする分野です。特に、テクノロジーが牽引力となる分、技術の理解に付いていけないプレイヤーでは業界の高度化に対応することはもちろん、流れを作っていくということも到底難しくなるでしょう。

その意味で、エコシステムの繁栄に貢献する「庭師」のような存在を目指しているFractonが技術を置いてけぼりにすることはあり得ません。このような背景からFractonは開発会社ではないにも関わらずCTOをおき技術とちゃんと向き合う姿勢をチームとしても大事にしています。

まとめ

私自身、気付けば一年半ほどこの少し特殊なポジションでやっています。このポジションならではの、そして自分ならではのユニークな価値提供は何でどんなスキルが必要なのか、なんとなく見えてきた手応えがあります。

このブログでは、日頃の業務で行ったリサーチや開発で気づいたことや躓いたことを発信することで、業界や今後参入したい方々の力に少しでもなればいいなと思っています。

なにぶん至らないところも多いと思いますが、このブログで今後発信される内容が議論のきっかけになれば幸いです!