Requiring a specific caller

We can use msg.sender in combination with a require statement to check who's calling our contract functions and limit access to specific addresses. Let's start by restricting who's allowed to resetBoard. For example, we could update the function to something like this:

    function resetBoard() public {
        require(msg.sender == ???, "Unauthorized");
        delete board;
    }

What address should we check against in the right hand side of this comparison? Recall that in our test environment, the caller contract address is 0xb4c79dab8f259c7aee6e5b2aa729821864227e84. We could hardcode it like this:

    function resetBoard() public {
        require(
            msg.sender == address(0xb4c79dab8f259c7aee6e5b2aa729821864227e84),
            "Unauthorized"
        );
        delete board;
    }

But this won't work if anyone besides our test contract calls resetBoard. Instead, we need to set and store the address in a state variable. Something like this:

    address public owner;

    constructor(address _owner) {
        owner = _owner;
    }

    function resetBoard() public {
        require(
            msg.sender == owner,
            "Unauthorized"
        );
        delete board;
    }

Let's start with a test, then update the implementation. We'll define an OWNER address as a constant in our test contract, pass it as an argument to our game during setup, and refer to it in our test.

    address internal constant OWNER = address(1);

    function setUp() public {
        ttt = new TicTacToken(OWNER);
    }

    function test_contract_owner() public {
        assertEq(ttt.owner(), OWNER);
    }

We'll need to update the constructor first:

$ forge test
Error:
   0: Compiler run failed
      TypeError: Wrong argument count for function call: 1 arguments given but expected 0.
        --> /Users/ecm/Projects/ttt-book-code/src/test/TicTacToken.t.sol:32:15:
         |
      32 |         ttt = new TicTacToken(OWNER);
         |               ^^^^^^^^^^^^^^^^^^^^^^

Let's add a state variable and set it in the constructor:

    address public owner;

    constructor(address _owner) {
        owner = _owner;
    }

Looks good:

$ forge test
Running 19 tests for src/test/TicTacToken.t.sol:TicTacTokenTest
[PASS] test_can_mark_space_with_O() (gas: 74789)
[PASS] test_can_mark_space_with_X() (gas: 51124)
[PASS] test_cannot_mark_space_with_Z() (gas: 10687)
[PASS] test_cannot_overwrite_marked_space() (gas: 56705)
[PASS] test_checks_for_antidiagonal_win() (gas: 183739)
[PASS] test_checks_for_diagonal_win() (gas: 161865)
[PASS] test_checks_for_horizontal_win() (gas: 159915)
[PASS] test_checks_for_horizontal_win_row2() (gas: 160241)
[PASS] test_checks_for_vertical_win() (gas: 182570)
[PASS] test_contract_owner() (gas: 7706)
[PASS] test_draw_returns_no_winner() (gas: 227181)
[PASS] test_empty_board_returns_no_winner() (gas: 32236)
[PASS] test_game_in_progress_returns_no_winner() (gas: 75903)
[PASS] test_get_board() (gas: 29218)
[PASS] test_has_empty_board() (gas: 32195)
[PASS] test_msg_sender() (gas: 238455)
[PASS] test_reset_board() (gas: 125168)
[PASS] test_symbols_must_alternate() (gas: 56453)
[PASS] test_tracks_current_turn() (gas: 76749)
Test result: ok. 19 passed; 0 failed; finished in 3.70ms

We still haven't added a check against the msg.sender in our resetBoard function. Let's first add a couple tests. We can use a new cheatcode to manipulate msg.sender in our tests. The vm.prank cheatcode sets the msg.sender address for the following line, similar to how vm.expectRevert applies to the immediately following statement.

    function test_owner_can_reset_board() public {
        vm.prank(OWNER);
        ttt.resetBoard();
    }

    function test_non_owner_cannot_reset_board() public {
        vm.expectRevert("Unauthorized");
        ttt.resetBoard();
    }

In the first test above, we set msg.sender to the OWNER address before calling ttt.resetBoard(). In the second, msg.sender will be the address of our test contract, and we expect the call to resetBoard to revert.

Our tests fail as expected:

$ forge test
Running 21 tests for src/test/TicTacToken.t.sol:TicTacTokenTest
[PASS] test_can_mark_space_with_O() (gas: 74767)
[PASS] test_can_mark_space_with_X() (gas: 51124)
[PASS] test_cannot_mark_space_with_Z() (gas: 10665)
[PASS] test_cannot_overwrite_marked_space() (gas: 56705)
[PASS] test_checks_for_antidiagonal_win() (gas: 183739)
[PASS] test_checks_for_diagonal_win() (gas: 161865)
[PASS] test_checks_for_horizontal_win() (gas: 159915)
[PASS] test_checks_for_horizontal_win_row2() (gas: 160241)
[PASS] test_checks_for_vertical_win() (gas: 182548)
[PASS] test_contract_owner() (gas: 7706)
[PASS] test_draw_returns_no_winner() (gas: 227203)
[PASS] test_empty_board_returns_no_winner() (gas: 32347)
[PASS] test_game_in_progress_returns_no_winner() (gas: 75903)
[PASS] test_get_board() (gas: 29218)
[PASS] test_has_empty_board() (gas: 32195)
[PASS] test_msg_sender() (gas: 238455)
[FAIL. Reason: Call did not revert as expected] test_non_owner_cannot_reset_board() (gas: 30817)
[PASS] test_owner_can_reset_board() (gas: 30799)
[PASS] test_reset_board() (gas: 125168)
[PASS] test_symbols_must_alternate() (gas: 56453)
[PASS] test_tracks_current_turn() (gas: 76749)
Test result: FAILED. 20 passed; 1 failed; finished in 3.44ms

Failed tests:
[FAIL. Reason: Call did not revert as expected] test_non_owner_cannot_reset_board() (gas: 30817)

Let's update resetBoard to add a require statement:

    function resetBoard() public {
        require(
            msg.sender == owner,
            "Unauthorized"
        );
        delete board;
    }

Run the tests once more...

Running 21 tests for src/test/TicTacToken.t.sol:TicTacTokenTest
[PASS] test_can_mark_space_with_O() (gas: 74767)
[PASS] test_can_mark_space_with_X() (gas: 51124)
[PASS] test_cannot_mark_space_with_Z() (gas: 10676)
[PASS] test_cannot_overwrite_marked_space() (gas: 56705)
[PASS] test_checks_for_antidiagonal_win() (gas: 183739)
[PASS] test_checks_for_diagonal_win() (gas: 161865)
[PASS] test_checks_for_horizontal_win() (gas: 159915)
[PASS] test_checks_for_horizontal_win_row2() (gas: 160241)
[PASS] test_checks_for_vertical_win() (gas: 182548)
[PASS] test_contract_owner() (gas: 7706)
[PASS] test_draw_returns_no_winner() (gas: 227203)
[PASS] test_empty_board_returns_no_winner() (gas: 32347)
[PASS] test_game_in_progress_returns_no_winner() (gas: 75903)
[PASS] test_get_board() (gas: 29218)
[PASS] test_has_empty_board() (gas: 32195)
[PASS] test_msg_sender() (gas: 238455)
[PASS] test_non_owner_cannot_reset_board() (gas: 12706)
[PASS] test_owner_can_reset_board() (gas: 32939)
[FAIL. Reason: Unauthorized] test_reset_board() (gas: 147658)
[PASS] test_symbols_must_alternate() (gas: 56453)
[PASS] test_tracks_current_turn() (gas: 76749)
Test result: FAILED. 20 passed; 1 failed; finished in 3.25ms

Failed tests:
[FAIL. Reason: Unauthorized] test_reset_board() (gas: 147658)

Encountered a total of 1 failing tests, 20 tests succeeded

Our latest two tests both passed, but we've caused our previous resetBoard test to fail. We'll have to add a vm.prank there, too:

    function test_reset_board() public {
        ttt.markSpace(3, X);
        ttt.markSpace(0, O);
        ttt.markSpace(4, X);
        ttt.markSpace(1, O);
        ttt.markSpace(5, X);
        vm.prank(OWNER);
        ttt.resetBoard();
        uint256[9] memory expected = [
            EMPTY,
            EMPTY,
            EMPTY,
            EMPTY,
            EMPTY,
            EMPTY,
            EMPTY,
            EMPTY,
            EMPTY
        ];
        uint256[9] memory actual = ttt.getBoard();

        for (uint256 i = 0; i < 9; i++) {
            assertEq(actual[i], expected[i]);
        }
    }

One more test run and we should be all green again:

$ forge test
Running 21 tests for src/test/TicTacToken.t.sol:TicTacTokenTest
[PASS] test_can_mark_space_with_O() (gas: 74767)
[PASS] test_can_mark_space_with_X() (gas: 51124)
[PASS] test_cannot_mark_space_with_Z() (gas: 10676)
[PASS] test_cannot_overwrite_marked_space() (gas: 56705)
[PASS] test_checks_for_antidiagonal_win() (gas: 183739)
[PASS] test_checks_for_diagonal_win() (gas: 161865)
[PASS] test_checks_for_horizontal_win() (gas: 159915)
[PASS] test_checks_for_horizontal_win_row2() (gas: 160241)
[PASS] test_checks_for_vertical_win() (gas: 182548)
[PASS] test_contract_owner() (gas: 7706)
[PASS] test_draw_returns_no_winner() (gas: 227203)
[PASS] test_empty_board_returns_no_winner() (gas: 32347)
[PASS] test_game_in_progress_returns_no_winner() (gas: 75903)
[PASS] test_get_board() (gas: 29218)
[PASS] test_has_empty_board() (gas: 32195)
[PASS] test_msg_sender() (gas: 238455)
[PASS] test_non_owner_cannot_reset_board() (gas: 12706)
[PASS] test_owner_can_reset_board() (gas: 32939)
[PASS] test_reset_board() (gas: 130859)
[PASS] test_symbols_must_alternate() (gas: 56453)
[PASS] test_tracks_current_turn() (gas: 76749)
Test result: ok. 21 passed; 0 failed; finished in 4.46ms