Awarding points with a token
Next, we need to actually use our new token from within our Tic Tac Toe game. Let's update our existing tests to call token.balanceOf
rather than ttt.totalPoints
. We'll also need to import our new Token.sol
contract, set it up in our test setup block, and pass its address through to our TicTacToken
contract:
import "../Token.sol";
contract TicTacTokenTest is DSTest {
Token internal token;
function setUp() public {
token = new Token();
ttt = new TicTacToken(OWNER, address(token));
playerX = new User(PLAYER_X, ttt, vm);
playerO = new User(PLAYER_O, ttt, vm);
ttt.newGame(PLAYER_X, PLAYER_O);
}
function test_increments_win_count_on_win() public {
playerX.markSpace(0, 0);
playerO.markSpace(0, 3);
playerX.markSpace(0, 1);
playerO.markSpace(0, 4);
playerX.markSpace(0, 2);
assertEq(token.balanceOf(PLAYER_X), 1);
ttt.newGame(PLAYER_X, PLAYER_O);
playerX.markSpace(1, 1);
playerO.markSpace(1, 2);
playerX.markSpace(1, 3);
playerO.markSpace(1, 4);
playerX.markSpace(1, 5);
playerO.markSpace(1, 6);
assertEq(token.balanceOf(PLAYER_O), 1);
}
function test_three_move_win_X() public {
// x | x | x
// o | o | .
// . | . | .
playerX.markSpace(0, 0);
playerO.markSpace(0, 3);
playerX.markSpace(0, 1);
playerO.markSpace(0, 4);
playerX.markSpace(0, 2);
assertEq(token.balanceOf(PLAYER_X), 300);
}
function test_three_move_win_O() public {
// x | x | .
// o | o | o
// x | . | .
playerX.markSpace(0, 0);
playerO.markSpace(0, 3);
playerX.markSpace(0, 1);
playerO.markSpace(0, 4);
playerX.markSpace(0, 6);
playerO.markSpace(0, 5);
assertEq(token.balanceOf(PLAYER_O), 300);
}
function test_four_move_win_X() public {
// x | x | x
// o | o | .
// x | o | .
playerX.markSpace(0, 0);
playerO.markSpace(0, 3);
playerX.markSpace(0, 1);
playerO.markSpace(0, 4);
playerX.markSpace(0, 6);
playerO.markSpace(0, 7);
playerX.markSpace(0, 2);
assertEq(token.balanceOf(PLAYER_X), 200);
}
function test_four_move_win_O() public {
// x | x | .
// o | o | o
// x | o | x
playerX.markSpace(0, 0);
playerO.markSpace(0, 3);
playerX.markSpace(0, 1);
playerO.markSpace(0, 4);
playerX.markSpace(0, 6);
playerO.markSpace(0, 7);
playerX.markSpace(0, 8);
playerO.markSpace(0, 5);
assertEq(token.balanceOf(PLAYER_O), 200);
}
function test_five_move_win_X() public {
// x | x | x
// o | o | x
// x | o | o
playerX.markSpace(0, 0);
playerO.markSpace(0, 3);
playerX.markSpace(0, 1);
playerO.markSpace(0, 4);
playerX.markSpace(0, 6);
playerO.markSpace(0, 7);
playerX.markSpace(0, 5);
playerO.markSpace(0, 8);
playerX.markSpace(0, 2);
assertEq(token.balanceOf(PLAYER_X), 100);
}
Let's first update the TicTacToken
constructor to take the token address as a second argument:
constructor(address _owner, address _token) {
owner = _owner;
}
Now our tests will fail as expected:
Failed tests:
[FAIL] test_five_move_win_X() (gas: 468223)
[FAIL] test_four_move_win_O() (gas: 435191)
[FAIL] test_four_move_win_X() (gas: 398714)
[FAIL] test_three_move_win_O() (gas: 365703)
[FAIL] test_three_move_win_X() (gas: 329183)
In order to mint tokens from our game contract, we'll need to make an external call to the Token
contract. We can do so using an interface.
Since our Token
contract conforms to the ERC20 interface, we can start with the IERC20
interface provided by OpenZeppelin. Let's define this at the top of our TicTacToken.sol
contract:
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
interface IToken is IERC20 {}
However, since we've added our own mint
function on top of the standard interface, we'll need to define it ourselves:
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
interface IToken is IERC20 {
function mint(address account, uint256 amount) external;
}
Now we can create and store an instance of this interface from the token address. All together, it should look something like this:
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
interface IToken is IERC20 {
function mint(address account, uint256 amount) external;
}
contract TicTacToken {
IToken internal token;
constructor(address _owner, address _token) {
owner = _owner;
token = IToken(_token);
}
}
Finally, we can call it to award points. Rather than updating the internal totalPoints
mapping, we'll call token.mint
with the winner address and number of points earned:
function markSpace(uint256 id, uint256 space) public {
require(_validPlayer(id), "Unauthorized");
require(_validTurn(id), "Not your turn");
require(_emptySpace(id, space), "Already marked");
games[id].board[space] = _getSymbol(id, msg.sender);
games[id].turns++;
if(winner(id) != 0) {
address winnerAddress = _getAddress(id, winner(id));
totalWins[winnerAddress] += 1;
token.mint(winnerAddress, _pointsEarned(id));
}
}
Run the tests...
Test result: FAILED. 20 passed; 13 failed; finished in 4.65ms
Failed tests:
[FAIL. Reason: Ownable: caller is not the owner] test_checks_for_antidiagonal_win() (gas: 340300)
[FAIL. Reason: Ownable: caller is not the owner] test_checks_for_diagonal_win() (gas: 303879)
[FAIL. Reason: Ownable: caller is not the owner] test_checks_for_horizontal_win() (gas: 296252)
[FAIL. Reason: Ownable: caller is not the owner] test_checks_for_horizontal_win_row2() (gas: 297474)
[FAIL. Reason: Ownable: caller is not the owner] test_checks_for_vertical_win() (gas: 335265)
[FAIL. Reason: Ownable: caller is not the owner] test_five_move_win_X() (gas: 435268)
[FAIL. Reason: Ownable: caller is not the owner] test_four_move_win_O() (gas: 402235)
[FAIL. Reason: Ownable: caller is not the owner] test_four_move_win_X() (gas: 365747)
[FAIL. Reason: Ownable: caller is not the owner] test_games_are_isolated() (gas: 335298)
[FAIL. Reason: Ownable: caller is not the owner] test_increments_win_count_on_win() (gas: 296185)
[FAIL. Reason: Ownable: caller is not the owner] test_reset_board() (gas: 297506)
[FAIL. Reason: Ownable: caller is not the owner] test_three_move_win_O() (gas: 332736)
[FAIL. Reason: Ownable: caller is not the owner] test_three_move_win_X() (gas: 296227)
Encountered a total of 13 failing tests, 26 tests succeeded
Eek! Our onlyOwner
modifier is working as expected, but our tests are failing because our game contract is not the token owner. (By default, the owner of an Ownable
contract is its deployer account address. In this case, that's our test contract.)
OpenZeppelin Ownable
provides a transferOwnership
method that we can call as part of our test setup to reassign ownership to our game contract:
function setUp() public {
token = new Token();
ttt = new TicTacToken(OWNER, address(token));
playerX = new User(PLAYER_X, ttt, vm);
playerO = new User(PLAYER_O, ttt, vm);
ttt.newGame(PLAYER_X, PLAYER_O);
token.transferOwnership(address(ttt));
}
With this change in place, our tests should now pass, and we can remove the now unused totalPoints
mapping:
Running 33 tests for src/test/TicTacToken.t.sol:TicTacTokenTest
[PASS] test_auth_nonplayer_cannot_mark_space() (gas: 15194)
[PASS] test_auth_playerO_can_mark_space() (gas: 121029)
[PASS] test_auth_playerX_can_mark_space() (gas: 84760)
[PASS] test_can_mark_space_with_O() (gas: 145822)
[PASS] test_can_mark_space_with_X() (gas: 98209)
[PASS] test_cannot_overwrite_marked_space() (gas: 110965)
[PASS] test_checks_for_antidiagonal_win() (gas: 399654)
[PASS] test_checks_for_diagonal_win() (gas: 362891)
[PASS] test_checks_for_horizontal_win() (gas: 353368)
[PASS] test_checks_for_horizontal_win_row2() (gas: 354906)
[PASS] test_checks_for_vertical_win() (gas: 393355)
[PASS] test_contract_owner() (gas: 7785)
[PASS] test_creates_new_game() (gas: 59164)
[PASS] test_draw_returns_no_winner() (gas: 361524)
[PASS] test_empty_board_returns_no_winner() (gas: 33955)
[PASS] test_five_move_win_X() (gas: 484602)
[PASS] test_four_move_win_O() (gas: 451570)
[PASS] test_four_move_win_X() (gas: 415093)
[PASS] test_game_in_progress_returns_no_winner() (gas: 105331)
[PASS] test_games_are_isolated() (gas: 746305)
[PASS] test_get_board() (gas: 29384)
[PASS] test_increments_win_count_on_win() (gas: 725272)
[PASS] test_non_owner_cannot_reset_board() (gas: 12844)
[PASS] test_owner_can_reset_board() (gas: 33137)
[PASS] test_playerO_win_count_starts_at_zero() (gas: 7800)
[PASS] test_playerX_win_count_starts_at_zero() (gas: 7849)
[PASS] test_reset_board() (gas: 283243)
[PASS] test_stores_player_O() (gas: 12342)
[PASS] test_stores_player_X() (gas: 12341)
[PASS] test_symbols_must_alternate() (gas: 97712)
[PASS] test_three_move_win_O() (gas: 382082)
[PASS] test_three_move_win_X() (gas: 345562)
[PASS] test_tracks_current_turn() (gas: 145010)
Test result: ok. 33 passed; 0 failed; finished in 5.49ms
We've created an ERC20 token, assigned ownership to our game, and used it to award points to the winner.