From 6d892669b28cd1c81d3342d6fde9edef19058cad Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 10 Jul 2024 20:26:58 +0200 Subject: [PATCH] feat: add experiment jackpot game --- src/LastCallJackpot.sol | 53 +++++++++++++++++++ test/LastCallJackpot.t.sol | 105 +++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 src/LastCallJackpot.sol create mode 100644 test/LastCallJackpot.t.sol diff --git a/src/LastCallJackpot.sol b/src/LastCallJackpot.sol new file mode 100644 index 0000000..77b58e8 --- /dev/null +++ b/src/LastCallJackpot.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.21; + +/// @notice Interface for TinyENS. +interface ILastCallJackpot { + /// @notice Call the contract and attempt to win the jackpot. + function call() external payable; +} + +/// @title A game where the last caller, before a 10-block inactivity period, wins the entire contract's ETH balance. +/// @author leovct +/// @notice https://www.paradigm.xyz/2024/06/paradigm-fellowship-2024 +/// An Ethereum contract is funded with 1,000 ETH. It costs 1 ETH to call, which is added to the balance. +/// If the contract isn't called for 10 blocks, the last caller gets the entire ETH balance. +/// How might this game unfold and end? Describe your thinking. +contract LastCallJackpot is ILastCallJackpot { + /// @notice Track the block number when the last call occurred. + uint256 public lastBlock; + /// @notice Store the address of the last caller. + address public lastCaller; + + /// @notice Log each call made to the contract. + event Called(address indexed caller, uint256 blockNumber); + /// @notice Log the transfer of the jackpot amount to the winner. + event WinnerPaid(address indexed winner, uint256 amount); + + constructor() payable { + require(msg.value == 50 ether, "Fund the contract with 1000 ETH"); + lastBlock = block.number; + } + + function call() external payable { + require(msg.value == 1 ether, "Call the contract with 1 ETH"); + + if (block.number >= lastBlock + 10) { + // Pay the winner. + uint256 balance = address(this).balance; + address winner = lastCaller; + + // Update state before external call + lastBlock = block.number; + lastCaller = msg.sender; + + payable(winner).transfer(balance); + emit WinnerPaid(winner, balance); + } else { + // Update the last block and caller. + lastBlock = block.number; + lastCaller = msg.sender; + emit Called(msg.sender, block.number); + } + } +} diff --git a/test/LastCallJackpot.t.sol b/test/LastCallJackpot.t.sol new file mode 100644 index 0000000..56eb652 --- /dev/null +++ b/test/LastCallJackpot.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.21; + +import "../src/LastCallJackpot.sol"; +import "@forge-std/Test.sol"; + +contract LastCallJackpotTest is Test { + LastCallJackpot private lastCallJackpot; + + address private owner = makeAddr("owner"); + address private alice = makeAddr("alice"); + address private bob = makeAddr("bob"); + + function setUp() public { + // Fund accounts. + vm.deal(owner, 10000 ether); + vm.deal(alice, 1000 ether); + vm.deal(bob, 1000 ether); + + // Deploy contract. + vm.startPrank(owner); + lastCallJackpot = new LastCallJackpot{value: 1000 ether}(); + console2.log("LastCallJackpot deployed"); + vm.stopPrank(); + } + + // This is just a test to make sure the call method works. + function test_SingleCall() public { + vm.startPrank(alice); + lastCallJackpot.call{value: 1 ether}(); + vm.stopPrank(); + + assertEq(lastCallJackpot.lastBlock(), block.number); + assertEq(lastCallJackpot.lastCaller(), alice); + assertEq(address(lastCallJackpot).balance, 1001 ether); + assertEq(alice.balance, 999 ether); + console2.log("Alice calls the contract"); + } + + // This is another test to make sure that one can win the jackpot according to the rules. + function test_WinJackpot() public { + uint256 bn = block.number; + + // Alice calls the contract. + vm.startPrank(alice); + lastCallJackpot.call{value: 1 ether}(); + vm.stopPrank(); + + assertEq(lastCallJackpot.lastBlock(), block.number); + assertEq(lastCallJackpot.lastCaller(), alice); + assertEq(address(lastCallJackpot).balance, 1001 ether); + assertEq(alice.balance, 999 ether); + console2.log("Alice calls the contract"); + + // 10 blocks have passed without anyone calling the contract. + vm.roll(bn + 10); + console2.log("10 blocks have passed"); + + // Bob calls the contract and Alice wins the jackpot. + vm.startPrank(bob); + lastCallJackpot.call{value: 1 ether}(); + vm.stopPrank(); + + assertEq(lastCallJackpot.lastBlock(), block.number); + assertEq(lastCallJackpot.lastCaller(), bob); + console2.log("Bob calls the contract"); + + assertEq(address(lastCallJackpot).balance, 0 ether); + assertEq(alice.balance, 999 ether + 1_001 ether + 1 ether); + console2.log("Alice wins the jackpot"); + } + + // This is another test to make sure that one can only win the jackpot if nobody called the contract for 10 blocks. + // This scenario + function test_MultiCall() public { + uint256 bn = block.number; + + // Alice calls the contract. + vm.startPrank(alice); + lastCallJackpot.call{value: 1 ether}(); + vm.stopPrank(); + + assertEq(lastCallJackpot.lastBlock(), block.number); + assertEq(lastCallJackpot.lastCaller(), alice); + assertEq(address(lastCallJackpot).balance, 1001 ether); + assertEq(alice.balance, 999 ether); + console2.log("Alice calls the contract"); + + // 9 blocks have passed without anyone calling the contract. + vm.roll(bn + 9); + console2.log("9 blocks have passed"); + + // Bob calls the contract and nobody wins the jackpot. + vm.startPrank(bob); + lastCallJackpot.call{value: 1 ether}(); + vm.stopPrank(); + + assertEq(lastCallJackpot.lastBlock(), block.number); + assertEq(lastCallJackpot.lastCaller(), bob); + assertEq(address(lastCallJackpot).balance, 1002 ether); + assertEq(bob.balance, 999 ether); + console2.log("Bob calls the contract"); + console2.log("Nobody wins the jackpot"); + } +}