|
// SPDX-License-Identifier: MIT |
|
pragma solidity ^0.8.23; |
|
|
|
import "forge-std/Test.sol"; |
|
import "../../src/ButtonGame.sol"; |
|
import "../../src/interfaces/IButtonGame.sol"; |
|
|
|
contract ButtonGameTest is Test { |
|
ButtonGame public game; |
|
|
|
// Test addresses |
|
address public owner = address(0x1); |
|
address public alice = address(0x2); |
|
address public bob = address(0x3); |
|
address public charlie = address(0x4); |
|
|
|
// Test parameters |
|
uint256 public constant INITIAL_PRICE = 0.01 ether; |
|
uint256 public constant PRICE_MULTIPLIER = 15; // 1.5x |
|
uint256 public constant GAME_DURATION = 1 hours; |
|
|
|
// Events |
|
event ButtonPressed(address indexed player, uint256 amountPaid, uint256 newPrice, uint256 totalPresses); |
|
event GameEnded(address indexed winner, uint256 prizeAmount, uint256 totalPresses); |
|
event GamePaused(address indexed account); |
|
event GameUnpaused(address indexed account); |
|
event GameInitialized(uint256 initialPrice, uint256 priceMultiplier, uint256 gameDuration); |
|
|
|
function setUp() public { |
|
vm.label(owner, "Owner"); |
|
vm.label(alice, "Alice"); |
|
vm.label(bob, "Bob"); |
|
vm.label(charlie, "Charlie"); |
|
|
|
vm.prank(owner); |
|
game = new ButtonGame(); |
|
} |
|
|
|
// ============ Deployment Tests ============ |
|
|
|
function test_deployment() public { |
|
assertEq(game.owner(), owner); |
|
assertEq(uint256(game.gameState()), uint256(IButtonGame.GameState.Paused)); |
|
assertEq(game.currentWinner(), address(0)); |
|
assertEq(game.currentPrice(), 0); |
|
assertEq(game.prizePool(), 0); |
|
assertEq(game.lastPressTime(), 0); |
|
assertEq(game.totalPresses(), 0); |
|
} |
|
|
|
// ============ Initialization Tests ============ |
|
|
|
function test_initialize_success() public { |
|
vm.prank(owner); |
|
vm.expectEmit(true, true, true, true); |
|
emit GameInitialized(INITIAL_PRICE, PRICE_MULTIPLIER, GAME_DURATION); |
|
game.initialize(INITIAL_PRICE, PRICE_MULTIPLIER, GAME_DURATION); |
|
|
|
assertEq(game.currentPrice(), INITIAL_PRICE); |
|
assertEq(game.priceMultiplier(), PRICE_MULTIPLIER); |
|
assertEq(game.gameDuration(), GAME_DURATION); |
|
assertEq(uint256(game.gameState()), uint256(IButtonGame.GameState.Active)); |
|
} |
|
|
|
function test_initialize_onlyOwner_reverts() public { |
|
vm.prank(alice); |
|
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", alice)); |
|
game.initialize(INITIAL_PRICE, PRICE_MULTIPLIER, GAME_DURATION); |
|
} |
|
|
|
function test_initialize_alreadyInitialized_reverts() public { |
|
vm.startPrank(owner); |
|
game.initialize(INITIAL_PRICE, PRICE_MULTIPLIER, GAME_DURATION); |
|
|
|
vm.expectRevert(IButtonGame.AlreadyInitialized.selector); |
|
game.initialize(INITIAL_PRICE, PRICE_MULTIPLIER, GAME_DURATION); |
|
vm.stopPrank(); |
|
} |
|
|
|
function test_initialize_invalidParameters_reverts() public { |
|
vm.startPrank(owner); |
|
|
|
// Zero initial price |
|
vm.expectRevert(IButtonGame.InvalidParameters.selector); |
|
game.initialize(0, PRICE_MULTIPLIER, GAME_DURATION); |
|
|
|
// Price multiplier less than 10 |
|
vm.expectRevert(IButtonGame.InvalidParameters.selector); |
|
game.initialize(INITIAL_PRICE, 9, GAME_DURATION); |
|
|
|
// Zero game duration |
|
vm.expectRevert(IButtonGame.InvalidParameters.selector); |
|
game.initialize(INITIAL_PRICE, PRICE_MULTIPLIER, 0); |
|
|
|
vm.stopPrank(); |
|
} |
|
|
|
// ============ Press Button Tests ============ |
|
|
|
function test_pressButton_firstPress() public { |
|
_initializeGame(); |
|
|
|
vm.deal(alice, 1 ether); |
|
vm.prank(alice); |
|
vm.expectEmit(true, true, true, true); |
|
emit ButtonPressed(alice, INITIAL_PRICE, (INITIAL_PRICE * PRICE_MULTIPLIER) / 10, 1); |
|
game.pressButton{value: INITIAL_PRICE}(); |
|
|
|
assertEq(game.currentWinner(), alice); |
|
assertEq(game.prizePool(), INITIAL_PRICE); |
|
assertEq(game.lastPressTime(), block.timestamp); |
|
assertEq(game.totalPresses(), 1); |
|
assertEq(game.currentPrice(), (INITIAL_PRICE * PRICE_MULTIPLIER) / 10); |
|
assertEq(game.getPlayerTotalSpent(alice), INITIAL_PRICE); |
|
assertEq(game.getPlayerPressCount(alice), 1); |
|
} |
|
|
|
function test_pressButton_multiplePresses() public { |
|
_initializeGame(); |
|
|
|
// Alice presses first |
|
vm.deal(alice, 1 ether); |
|
vm.prank(alice); |
|
game.pressButton{value: INITIAL_PRICE}(); |
|
|
|
uint256 secondPrice = game.currentPrice(); |
|
|
|
// Bob presses second |
|
vm.deal(bob, 1 ether); |
|
vm.prank(bob); |
|
game.pressButton{value: secondPrice}(); |
|
|
|
assertEq(game.currentWinner(), bob); |
|
assertEq(game.prizePool(), INITIAL_PRICE + secondPrice); |
|
assertEq(game.totalPresses(), 2); |
|
assertEq(game.currentPrice(), (secondPrice * PRICE_MULTIPLIER) / 10); |
|
} |
|
|
|
function test_pressButton_incorrectPayment_reverts() public { |
|
_initializeGame(); |
|
|
|
vm.deal(alice, 1 ether); |
|
vm.prank(alice); |
|
|
|
// Too little |
|
vm.expectRevert(abi.encodeWithSelector( |
|
IButtonGame.IncorrectPayment.selector, |
|
INITIAL_PRICE - 1, |
|
INITIAL_PRICE |
|
)); |
|
game.pressButton{value: INITIAL_PRICE - 1}(); |
|
|
|
// Too much |
|
vm.expectRevert(abi.encodeWithSelector( |
|
IButtonGame.IncorrectPayment.selector, |
|
INITIAL_PRICE + 1, |
|
INITIAL_PRICE |
|
)); |
|
game.pressButton{value: INITIAL_PRICE + 1}(); |
|
} |
|
|
|
function test_pressButton_whenPaused_reverts() public { |
|
_initializeGame(); |
|
|
|
vm.prank(owner); |
|
game.pause(); |
|
|
|
vm.deal(alice, 1 ether); |
|
vm.prank(alice); |
|
vm.expectRevert(abi.encodeWithSignature("EnforcedPause()")); |
|
game.pressButton{value: INITIAL_PRICE}(); |
|
} |
|
|
|
function test_pressButton_afterGameEnded_reverts() public { |
|
_initializeGame(); |
|
|
|
// Alice presses |
|
vm.deal(alice, 1 ether); |
|
vm.prank(alice); |
|
game.pressButton{value: INITIAL_PRICE}(); |
|
|
|
// Advance time past game duration |
|
skip(GAME_DURATION + 1); |
|
|
|
// Bob tries to press after game ended |
|
vm.deal(bob, 1 ether); |
|
vm.prank(bob); |
|
vm.expectRevert(IButtonGame.GameHasEnded.selector); |
|
game.pressButton{value: game.currentPrice()}(); |
|
} |
|
|
|
// ============ Claim Prize Tests ============ |
|
|
|
function test_claimPrize_success() public { |
|
_initializeGame(); |
|
|
|
// Alice presses and becomes winner |
|
vm.deal(alice, 1 ether); |
|
vm.prank(alice); |
|
game.pressButton{value: INITIAL_PRICE}(); |
|
|
|
uint256 aliceBalanceBefore = alice.balance; |
|
|
|
// Advance time past game duration |
|
skip(GAME_DURATION + 1); |
|
|
|
// Anyone can trigger claim |
|
vm.prank(bob); |
|
vm.expectEmit(true, true, true, true); |
|
emit GameEnded(alice, INITIAL_PRICE, 1); |
|
game.claimPrize(); |
|
|
|
assertEq(alice.balance, aliceBalanceBefore + INITIAL_PRICE); |
|
assertEq(game.prizePool(), 0); |
|
assertEq(uint256(game.gameState()), uint256(IButtonGame.GameState.Ended)); |
|
} |
|
|
|
function test_claimPrize_gameNotEnded_reverts() public { |
|
_initializeGame(); |
|
|
|
vm.deal(alice, 1 ether); |
|
vm.prank(alice); |
|
game.pressButton{value: INITIAL_PRICE}(); |
|
|
|
// Try to claim before game ends |
|
vm.prank(alice); |
|
vm.expectRevert(IButtonGame.GameNotEnded.selector); |
|
game.claimPrize(); |
|
} |
|
|
|
function test_claimPrize_alreadyClaimed_reverts() public { |
|
_initializeGame(); |
|
|
|
vm.deal(alice, 1 ether); |
|
vm.prank(alice); |
|
game.pressButton{value: INITIAL_PRICE}(); |
|
|
|
skip(GAME_DURATION + 1); |
|
|
|
// First claim succeeds |
|
vm.prank(bob); |
|
game.claimPrize(); |
|
|
|
// Second claim fails |
|
vm.prank(charlie); |
|
vm.expectRevert(IButtonGame.PrizeAlreadyClaimed.selector); |
|
game.claimPrize(); |
|
} |
|
|
|
function test_claimPrize_noWinner_reverts() public { |
|
_initializeGame(); |
|
|
|
// No one pressed button |
|
skip(GAME_DURATION + 1); |
|
|
|
vm.prank(alice); |
|
vm.expectRevert(IButtonGame.GameNotEnded.selector); |
|
game.claimPrize(); |
|
} |
|
|
|
// ============ Pause/Unpause Tests ============ |
|
|
|
function test_pause_success() public { |
|
_initializeGame(); |
|
|
|
vm.prank(owner); |
|
vm.expectEmit(true, false, false, true); |
|
emit GamePaused(owner); |
|
game.pause(); |
|
|
|
assertEq(uint256(game.gameState()), uint256(IButtonGame.GameState.Paused)); |
|
assertTrue(game.paused()); |
|
} |
|
|
|
function test_pause_onlyOwner_reverts() public { |
|
_initializeGame(); |
|
|
|
vm.prank(alice); |
|
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", alice)); |
|
game.pause(); |
|
} |
|
|
|
function test_unpause_success() public { |
|
_initializeGame(); |
|
|
|
vm.prank(owner); |
|
game.pause(); |
|
|
|
vm.prank(owner); |
|
vm.expectEmit(true, false, false, true); |
|
emit GameUnpaused(owner); |
|
game.unpause(); |
|
|
|
assertEq(uint256(game.gameState()), uint256(IButtonGame.GameState.Active)); |
|
assertFalse(game.paused()); |
|
} |
|
|
|
function test_unpause_afterGameEnded_reverts() public { |
|
_initializeGame(); |
|
|
|
// Alice presses |
|
vm.deal(alice, 1 ether); |
|
vm.prank(alice); |
|
game.pressButton{value: INITIAL_PRICE}(); |
|
|
|
// Pause the game |
|
vm.prank(owner); |
|
game.pause(); |
|
|
|
// Advance time past game duration |
|
skip(GAME_DURATION + 1); |
|
|
|
// Try to unpause after game ended |
|
vm.prank(owner); |
|
vm.expectRevert(IButtonGame.GameHasEnded.selector); |
|
game.unpause(); |
|
} |
|
|
|
// ============ View Function Tests ============ |
|
|
|
function test_timeRemaining() public { |
|
_initializeGame(); |
|
|
|
assertEq(game.timeRemaining(), 0); // No press yet |
|
|
|
vm.deal(alice, 1 ether); |
|
vm.prank(alice); |
|
game.pressButton{value: INITIAL_PRICE}(); |
|
|
|
assertEq(game.timeRemaining(), GAME_DURATION); |
|
|
|
skip(30 minutes); |
|
assertEq(game.timeRemaining(), 30 minutes); |
|
|
|
skip(31 minutes); |
|
assertEq(game.timeRemaining(), 0); |
|
} |
|
|
|
function test_hasEnded() public { |
|
_initializeGame(); |
|
|
|
assertFalse(game.hasEnded()); |
|
|
|
vm.deal(alice, 1 ether); |
|
vm.prank(alice); |
|
game.pressButton{value: INITIAL_PRICE}(); |
|
|
|
assertFalse(game.hasEnded()); |
|
|
|
skip(GAME_DURATION + 1); |
|
assertTrue(game.hasEnded()); |
|
} |
|
|
|
function test_getPressInfo() public { |
|
_initializeGame(); |
|
|
|
// No presses yet |
|
(address player, uint256 amount, uint256 timestamp) = game.getPressInfo(0); |
|
assertEq(player, address(0)); |
|
assertEq(amount, 0); |
|
assertEq(timestamp, 0); |
|
|
|
// Alice presses |
|
vm.deal(alice, 1 ether); |
|
vm.prank(alice); |
|
uint256 pressTime = block.timestamp; |
|
game.pressButton{value: INITIAL_PRICE}(); |
|
|
|
(player, amount, timestamp) = game.getPressInfo(0); |
|
assertEq(player, alice); |
|
assertEq(amount, INITIAL_PRICE); |
|
assertEq(timestamp, pressTime); |
|
|
|
// Out of bounds |
|
(player, amount, timestamp) = game.getPressInfo(1); |
|
assertEq(player, address(0)); |
|
assertEq(amount, 0); |
|
assertEq(timestamp, 0); |
|
} |
|
|
|
function test_playerStats() public { |
|
_initializeGame(); |
|
|
|
assertEq(game.getPlayerTotalSpent(alice), 0); |
|
assertEq(game.getPlayerPressCount(alice), 0); |
|
|
|
// Alice presses twice |
|
vm.deal(alice, 10 ether); |
|
vm.startPrank(alice); |
|
|
|
game.pressButton{value: INITIAL_PRICE}(); |
|
uint256 secondPrice = game.currentPrice(); |
|
game.pressButton{value: secondPrice}(); |
|
|
|
vm.stopPrank(); |
|
|
|
assertEq(game.getPlayerTotalSpent(alice), INITIAL_PRICE + secondPrice); |
|
assertEq(game.getPlayerPressCount(alice), 2); |
|
} |
|
|
|
// ============ Edge Case Tests ============ |
|
|
|
function test_pressButton_reentrancyProtection() public { |
|
_initializeGame(); |
|
|
|
// Deploy malicious contract |
|
MaliciousPlayer malicious = new MaliciousPlayer(game); |
|
vm.deal(address(malicious), 10 ether); |
|
|
|
// Attempt reentrancy attack |
|
vm.prank(address(malicious)); |
|
vm.expectRevert(); // Should revert due to reentrancy guard |
|
malicious.attack{value: INITIAL_PRICE}(); |
|
} |
|
|
|
function test_claimPrize_toContractWithoutReceive() public { |
|
_initializeGame(); |
|
|
|
// Deploy contract without receive/fallback |
|
NonReceivingContract nonReceiver = new NonReceivingContract(); |
|
|
|
// Contract presses button |
|
vm.deal(address(nonReceiver), 1 ether); |
|
vm.prank(address(nonReceiver)); |
|
game.pressButton{value: INITIAL_PRICE}(); |
|
|
|
skip(GAME_DURATION + 1); |
|
|
|
// Claim should fail due to transfer failure |
|
vm.expectRevert(IButtonGame.TransferFailed.selector); |
|
game.claimPrize(); |
|
} |
|
|
|
function test_priceCalculation_overflow() public { |
|
_initializeGame(); |
|
|
|
// Set up scenario where price could overflow |
|
vm.prank(owner); |
|
game = new ButtonGame(); |
|
|
|
// Initialize with very high initial price |
|
uint256 highPrice = type(uint256).max / 2; |
|
vm.prank(owner); |
|
game.initialize(highPrice, 20, GAME_DURATION); // 2x multiplier |
|
|
|
vm.deal(alice, highPrice); |
|
vm.prank(alice); |
|
vm.expectRevert(); // Should revert on overflow |
|
game.pressButton{value: highPrice}(); |
|
} |
|
|
|
// ============ Helper Functions ============ |
|
|
|
function _initializeGame() private { |
|
vm.prank(owner); |
|
game.initialize(INITIAL_PRICE, PRICE_MULTIPLIER, GAME_DURATION); |
|
} |
|
} |
|
|
|
// ============ Attack Contracts ============ |
|
|
|
contract MaliciousPlayer { |
|
ButtonGame public game; |
|
uint256 public attackCount; |
|
|
|
constructor(ButtonGame _game) { |
|
game = _game; |
|
} |
|
|
|
function attack() external payable { |
|
game.pressButton{value: msg.value}(); |
|
} |
|
|
|
receive() external payable { |
|
attackCount++; |
|
if (attackCount < 2) { |
|
// Try to re-enter |
|
game.pressButton{value: game.currentPrice()}(); |
|
} |
|
} |
|
} |
|
|
|
contract NonReceivingContract { |
|
// No receive or fallback function |
|
} |