Fracton テックブログ

FractonのEthereum周辺技術について発信しています

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を活用し、コントラクトの堅牢性を高めている。