Open6

foundryのテストを書くときにexpectRevertを使うとコケた件

MingleMingle(Thurendous)MingleMingle(Thurendous)

おきたこと

foundryでテストを書く際、ローレベルコールcallを使うときに、普通は成功するとステータスのtrueを返し、失敗するとfalseを返すのだ。

しかし、function expectRevert() external;を使うとなぜか絶対失敗してsuccessがfalseやろうというときにも、trueを返すことがある。

(bool success, bytes memory returndata) = address(xx).call(abi.encodeWithSignature("xxoo()"));

解決法

これはあかんということで調べたらこちらにたどり着いた。
https://book.getfoundry.sh/cheatcodes/expect-revert

Gochaがあって、expectRevert()を使うとexpectRevertの挙動が成功するかどうかを使って、ローレベルルコールが成功するか否かのsuccessを上書きしてしまうというらしい。

だから、expectRevert()を使うときには、successfalseになることは望めないというわけだ。

MingleMingle(Thurendous)MingleMingle(Thurendous)
    function testIfCallingFunctionDoesntExistThenRevert2() public {
        // Expect the revert
        // vm.expectRevert();

        // Attempt to call a non-existing function
        (bool success, bytes memory returndata) = address(tf).call(abi.encodeWithSignature("nonExistingFunction()"));

        // Log the success status
        console.log("success:", success);
        console.logBytes(returndata);

        // Ensure the call reverted
        assert(success == false && returndata.length == 0);
    }

こちらのcallを使った返り値は0xで空っぽのdataとなった。

// This is the log of `// Log the success status`
  success: false
  0x
MingleMingle(Thurendous)MingleMingle(Thurendous)

testするときに便利なコマンド

  • コントラクトのregexで探してそれだけをテストする用
forge test --mc TokenTest    
forge test --match-contract TokenTest
  • テストのregexで探してそれだけをテストする用
forge test --mt testStateVariables
forge test --match-test testStateVariables
MingleMingle(Thurendous)MingleMingle(Thurendous)

おきたこと


    function testTransfer() public {
        address to = makeAddr("to");
        token.transfer(to, 1_000_000 * 1e18);
        assertEq(token.balanceOf(to), 1_000_000 * 1e18);
        assertEq(token.balanceOf(fakeHolder), 999_000_000 * 1e18);
    }

上記のFoundryテストでtokenを送付する時に、テストが失敗した理由は、テストの途中で実行されたトランザクションが失敗したためです。具体的には、次のエラーが出ています:

[FAIL. Reason: ERC20InsufficientBalance(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496, 0, 1000000000000000000000000 [1e24])]

このエラーは、「ERC20InsufficientBalance」というカスタムエラーで、アカウントの残高が不足しているた
めにトランザクションが失敗したことを示しています。以下に考えられる原因と解決策を示します:

原因

  1. デフォルトアカウントの残高不足:
    • Foundryでは、デフォルトで最初のテストアカウント(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)からトランザクションが送信されます。このアカウントに十分なトークンがないため、トランザクションが失敗しています。
  2. 初期設定の不足:
    • token.transfer(to, 1_000_000 * 1e18)を実行する前に、テストアカウントにトークンを割り当てる初期設定が不足している可能性があります。

このように修正したらOK:

    function testTransfer() public {
        address to = makeAddr("to");
        vm.prank(fakeHolder);
        token.transfer(to, 1_000_000 * 1e18);
        assertEq(token.balanceOf(to), 1_000_000 * 1e18);
        assertEq(token.balanceOf(fakeHolder), 999_000_000 * 1e18);
    }
MingleMingle(Thurendous)MingleMingle(Thurendous)

testを書く時に、明示的にexpectRevertされるときのエラーの署名も含めて、しかもエラーには引数がある状態でチェックしたいときに、どうしたらいいかというサンプルコードはこちらです。

    function testIfCreateTokenFunctionFailsWhenFeeIsNotEnough() public {
        address someone = makeAddr("someone");
        vm.deal(someone, 1 ether);
        // test something
        bytes4 selector = bytes4(keccak256("NotEnoughFee(uint256)"));
        vm.expectRevert(abi.encodeWithSelector(selector, 50000000000000));
        // fee should be 645161290322580 / 1e18 = 0.000645161290322580
        // when eth price is 3100
        tf.createToken{value: 0.00005 ether}("Test Token Token", "TSTT", address(this), 1_000_000_000 * 1e18);
    }

    function testConstructorIfZeroAddressNotAllowed() public {
        bytes4 selector = bytes4(keccak256("OwnableInvalidOwner(address)"));
        vm.expectRevert(abi.encodeWithSelector(selector, address(0)));
        new TokenFactory(address(0), address(1), address(2));
    }

もう一つ簡単な書き方はこれ:

    function testIfCreateTokenFunctionFailsWhenInitialHolderIsZero() public {
        address someone = makeAddr("someone");
        vm.deal(someone, 1 ether);
        // test something
        vm.expectRevert(ZeroAddressNotAllowed.selector);
        contract.functionName{value: 0.01 ether}(address(0));
    }
MingleMingle(Thurendous)MingleMingle(Thurendous)

command例

vm.prank(<address>): 誰になりすます。
vm.startPrank(<address>): 誰かになりすます。ここまで同じだが、vm.stopPrank()が出現するまでずっとなりすます。