├── .env.example ├── img └── automation.png ├── .gitpod.yml ├── test ├── mocks │ ├── VRFCoordinatorV2Mock.sol │ └── LinkToken.sol ├── staging │ └── RaffleStagingTest.t.sol └── unit │ └── RaffleTest.t.sol ├── .gitignore ├── src ├── sublesson │ ├── ExampleRevert.sol │ ├── ExampleModulo.sol │ └── ExampleEvents.sol └── Raffle.sol ├── foundry.toml ├── .gitmodules ├── .github └── workflows │ └── test.yml ├── Makefile ├── script ├── DeployRaffle.s.sol ├── HelperConfig.s.sol └── Interactions.s.sol └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | PRIVATE_KEY=XXXXXXXXX 2 | RPC_URL=http://0.0.0.0:8545 3 | ETHERSCAN_API_KEY=XXXX -------------------------------------------------------------------------------- /img/automation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickAlphaC/foundry-smart-contract-lottery-f23/HEAD/img/automation.png -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - name: Install Foundry 3 | init: | 4 | curl -L https://foundry.paradigm.xyz | bash 5 | source /home/gitpod/.bashrc 6 | foundryup -------------------------------------------------------------------------------- /test/mocks/VRFCoordinatorV2Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol"; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | /broadcast/ 10 | 11 | # Docs 12 | docs/ 13 | 14 | # Dotenv file 15 | .env 16 | -------------------------------------------------------------------------------- /src/sublesson/ExampleRevert.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.18; 4 | 5 | contract ExampleRevert { 6 | error ExampleRevert__Error(); 7 | 8 | function revertWithError() public pure { 9 | if (false) { 10 | revert ExampleRevert__Error(); 11 | } 12 | } 13 | 14 | function revertWithRequire() public pure { 15 | require(true, "ExampleRevert__Error"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | remappings = ['@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/', '@solmate=lib/solmate/src/'] 6 | 7 | [etherscan] 8 | mainnet = { key = "${ETHERSCAN_API_KEY}" } 9 | sepolia = {key = "${ETHERSCAN_API_KEY}"} 10 | 11 | [rpc_endpoints] 12 | sepolia = "${SEPOLIA_RPC_URL}" 13 | 14 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/foundry-devops"] 2 | path = lib/foundry-devops 3 | url = https://github.com/Cyfrin/foundry-devops 4 | [submodule "lib/chainlink-brownie-contracts"] 5 | path = lib/chainlink-brownie-contracts 6 | url = https://github.com/smartcontractkit/chainlink-brownie-contracts 7 | [submodule "lib/forge-std"] 8 | path = lib/forge-std 9 | url = https://github.com/foundry-rs/forge-std 10 | [submodule "lib/solmate"] 11 | path = lib/solmate 12 | url = https://github.com/transmissions11/solmate 13 | -------------------------------------------------------------------------------- /src/sublesson/ExampleModulo.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.18; 4 | 5 | contract ExampleModulo { 6 | function getModTen(uint256 number) external pure returns (uint256) { 7 | // 10 % 10 = 0 8 | // 10 % 9 = 1 (10 / 9 = 1.??) 9 | // 2 % 2 = 0. 2 % 3 = 1. 2 % 6 = 0. 2 % 7 = 1 10 | return number % 10; 11 | } 12 | 13 | function getModTwo(uint256 number) external pure returns (uint256) { 14 | return number % 2; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /src/sublesson/ExampleEvents.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.18; 4 | 5 | contract ExampleEvents { 6 | uint256 favoriteNumber; 7 | event storedNumber( 8 | uint256 indexed oldNumber, 9 | uint256 indexed newNumber, 10 | uint256 addedNumber, 11 | address sender 12 | ); 13 | 14 | function store(uint256 _favoriteNumber) public { 15 | emit storedNumber( 16 | favoriteNumber, 17 | _favoriteNumber, 18 | _favoriteNumber + favoriteNumber, 19 | msg.sender 20 | ); 21 | favoriteNumber = _favoriteNumber; 22 | } 23 | 24 | function retrieve() public view returns (uint256) { 25 | return favoriteNumber; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include .env 2 | 3 | .PHONY: all test clean deploy fund help install snapshot format anvil 4 | 5 | DEFAULT_ANVIL_KEY := 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 6 | 7 | help: 8 | @echo "Usage:" 9 | @echo " make deploy [ARGS=...]\n example: make deploy ARGS=\"--network sepolia\"" 10 | @echo "" 11 | @echo " make fund [ARGS=...]\n example: make deploy ARGS=\"--network sepolia\"" 12 | 13 | all: clean remove install update build 14 | 15 | # Clean the repo 16 | clean :; forge clean 17 | 18 | # Remove modules 19 | remove :; rm -rf .gitmodules && rm -rf .git/modules/* && rm -rf lib && touch .gitmodules && git add . && git commit -m "modules" 20 | 21 | install :; forge install chainaccelorg/foundry-devops@0.0.11 --no-commit && forge install smartcontractkit/chainlink-brownie-contracts@0.6.1 --no-commit && forge install foundry-rs/forge-std@v1.5.3 --no-commit && forge install transmissions11/solmate@v6 --no-commit 22 | 23 | # Update Dependencies 24 | update:; forge update 25 | 26 | build:; forge build 27 | 28 | test :; forge test 29 | 30 | snapshot :; forge snapshot 31 | 32 | format :; forge fmt 33 | 34 | anvil :; anvil -m 'test test test test test test test test test test test junk' --steps-tracing --block-time 1 35 | 36 | NETWORK_ARGS := --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast 37 | 38 | ifeq ($(findstring --network sepolia,$(ARGS)),--network sepolia) 39 | NETWORK_ARGS := --rpc-url $(SEPOLIA_RPC_URL) --private-key $(PRIVATE_KEY) --broadcast --verify --etherscan-api-key $(ETHERSCAN_API_KEY) -vvvv 40 | endif 41 | 42 | deploy: 43 | @forge script script/DeployRaffle.s.sol:DeployRaffle $(NETWORK_ARGS) 44 | 45 | createSubscription: 46 | @forge script script/Interactions.s.sol:CreateSubscription $(NETWORK_ARGS) 47 | 48 | addConsumer: 49 | @forge script script/Interactions.s.sol:AddConsumer $(NETWORK_ARGS) 50 | 51 | fundSubscription: 52 | @forge script script/Interactions.s.sol:FundSubscription $(NETWORK_ARGS) 53 | 54 | 55 | -------------------------------------------------------------------------------- /test/mocks/LinkToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // @dev This contract has been adapted to fit with dappTools 4 | pragma solidity ^0.8.0; 5 | 6 | import "@solmate/tokens/ERC20.sol"; 7 | 8 | interface ERC677Receiver { 9 | function onTokenTransfer( 10 | address _sender, 11 | uint256 _value, 12 | bytes memory _data 13 | ) external; 14 | } 15 | 16 | contract LinkToken is ERC20 { 17 | uint256 constant INITIAL_SUPPLY = 1000000000000000000000000; 18 | uint8 constant DECIMALS = 18; 19 | 20 | constructor() ERC20("LinkToken", "LINK", DECIMALS) { 21 | _mint(msg.sender, INITIAL_SUPPLY); 22 | } 23 | 24 | event Transfer( 25 | address indexed from, 26 | address indexed to, 27 | uint256 value, 28 | bytes data 29 | ); 30 | 31 | /** 32 | * @dev transfer token to a contract address with additional data if the recipient is a contact. 33 | * @param _to The address to transfer to. 34 | * @param _value The amount to be transferred. 35 | * @param _data The extra data to be passed to the receiving contract. 36 | */ 37 | function transferAndCall( 38 | address _to, 39 | uint256 _value, 40 | bytes memory _data 41 | ) public virtual returns (bool success) { 42 | super.transfer(_to, _value); 43 | // emit Transfer(msg.sender, _to, _value, _data); 44 | emit Transfer(msg.sender, _to, _value, _data); 45 | if (isContract(_to)) { 46 | contractFallback(_to, _value, _data); 47 | } 48 | return true; 49 | } 50 | 51 | // PRIVATE 52 | 53 | function contractFallback( 54 | address _to, 55 | uint256 _value, 56 | bytes memory _data 57 | ) private { 58 | ERC677Receiver receiver = ERC677Receiver(_to); 59 | receiver.onTokenTransfer(msg.sender, _value, _data); 60 | } 61 | 62 | function isContract(address _addr) private view returns (bool hasCode) { 63 | uint256 length; 64 | assembly { 65 | length := extcodesize(_addr) 66 | } 67 | return length > 0; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /script/DeployRaffle.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {HelperConfig} from "./HelperConfig.s.sol"; 6 | import {Raffle} from "../src/Raffle.sol"; 7 | import {AddConsumer, CreateSubscription, FundSubscription} from "./Interactions.s.sol"; 8 | 9 | contract DeployRaffle is Script { 10 | function run() external returns (Raffle, HelperConfig) { 11 | HelperConfig helperConfig = new HelperConfig(); // This comes with our mocks! 12 | AddConsumer addConsumer = new AddConsumer(); 13 | ( 14 | uint64 subscriptionId, 15 | bytes32 gasLane, 16 | uint256 automationUpdateInterval, 17 | uint256 raffleEntranceFee, 18 | uint32 callbackGasLimit, 19 | address vrfCoordinatorV2, 20 | address link, 21 | uint256 deployerKey 22 | ) = helperConfig.activeNetworkConfig(); 23 | 24 | if (subscriptionId == 0) { 25 | CreateSubscription createSubscription = new CreateSubscription(); 26 | subscriptionId = createSubscription.createSubscription( 27 | vrfCoordinatorV2, 28 | deployerKey 29 | ); 30 | 31 | FundSubscription fundSubscription = new FundSubscription(); 32 | fundSubscription.fundSubscription( 33 | vrfCoordinatorV2, 34 | subscriptionId, 35 | link, 36 | deployerKey 37 | ); 38 | } 39 | 40 | vm.startBroadcast(deployerKey); 41 | Raffle raffle = new Raffle( 42 | subscriptionId, 43 | gasLane, 44 | automationUpdateInterval, 45 | raffleEntranceFee, 46 | callbackGasLimit, 47 | vrfCoordinatorV2 48 | ); 49 | vm.stopBroadcast(); 50 | 51 | // We already have a broadcast in here 52 | addConsumer.addConsumer( 53 | address(raffle), 54 | vrfCoordinatorV2, 55 | subscriptionId, 56 | deployerKey 57 | ); 58 | return (raffle, helperConfig); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /script/HelperConfig.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {VRFCoordinatorV2Mock} from "../test/mocks/VRFCoordinatorV2Mock.sol"; 5 | import {LinkToken} from "../test/mocks/LinkToken.sol"; 6 | import {Script} from "forge-std/Script.sol"; 7 | 8 | contract HelperConfig is Script { 9 | NetworkConfig public activeNetworkConfig; 10 | 11 | struct NetworkConfig { 12 | uint64 subscriptionId; 13 | bytes32 gasLane; 14 | uint256 automationUpdateInterval; 15 | uint256 raffleEntranceFee; 16 | uint32 callbackGasLimit; 17 | address vrfCoordinatorV2; 18 | address link; 19 | uint256 deployerKey; 20 | } 21 | 22 | uint256 public DEFAULT_ANVIL_PRIVATE_KEY = 23 | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; 24 | 25 | event HelperConfig__CreatedMockVRFCoordinator(address vrfCoordinator); 26 | 27 | constructor() { 28 | if (block.chainid == 11155111) { 29 | activeNetworkConfig = getSepoliaEthConfig(); 30 | } else { 31 | activeNetworkConfig = getOrCreateAnvilEthConfig(); 32 | } 33 | } 34 | 35 | function getMainnetEthConfig() 36 | public 37 | view 38 | returns (NetworkConfig memory mainnetNetworkConfig) 39 | { 40 | mainnetNetworkConfig = NetworkConfig({ 41 | subscriptionId: 0, // If left as 0, our scripts will create one! 42 | gasLane: 0x9fe0eebf5e446e3c998ec9bb19951541aee00bb90ea201ae456421a2ded86805, 43 | automationUpdateInterval: 30, // 30 seconds 44 | raffleEntranceFee: 0.01 ether, 45 | callbackGasLimit: 500000, // 500,000 gas 46 | vrfCoordinatorV2: 0x271682DEB8C4E0901D1a1550aD2e64D568E69909, 47 | link: 0x514910771AF9Ca656af840dff83E8264EcF986CA, 48 | deployerKey: vm.envUint("PRIVATE_KEY") 49 | }); 50 | } 51 | 52 | function getSepoliaEthConfig() 53 | public 54 | view 55 | returns (NetworkConfig memory sepoliaNetworkConfig) 56 | { 57 | sepoliaNetworkConfig = NetworkConfig({ 58 | subscriptionId: 0, // If left as 0, our scripts will create one! 59 | gasLane: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c, 60 | automationUpdateInterval: 30, // 30 seconds 61 | raffleEntranceFee: 0.01 ether, 62 | callbackGasLimit: 500000, // 500,000 gas 63 | vrfCoordinatorV2: 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625, 64 | link: 0x779877A7B0D9E8603169DdbD7836e478b4624789, 65 | deployerKey: vm.envUint("PRIVATE_KEY") 66 | }); 67 | } 68 | 69 | function getOrCreateAnvilEthConfig() 70 | public 71 | returns (NetworkConfig memory anvilNetworkConfig) 72 | { 73 | // Check to see if we set an active network config 74 | if (activeNetworkConfig.vrfCoordinatorV2 != address(0)) { 75 | return activeNetworkConfig; 76 | } 77 | 78 | uint96 baseFee = 0.25 ether; 79 | uint96 gasPriceLink = 1e9; 80 | 81 | vm.startBroadcast(DEFAULT_ANVIL_PRIVATE_KEY); 82 | VRFCoordinatorV2Mock vrfCoordinatorV2Mock = new VRFCoordinatorV2Mock( 83 | baseFee, 84 | gasPriceLink 85 | ); 86 | 87 | LinkToken link = new LinkToken(); 88 | vm.stopBroadcast(); 89 | 90 | emit HelperConfig__CreatedMockVRFCoordinator( 91 | address(vrfCoordinatorV2Mock) 92 | ); 93 | 94 | anvilNetworkConfig = NetworkConfig({ 95 | subscriptionId: 0, // If left as 0, our scripts will create one! 96 | gasLane: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c, // doesn't really matter 97 | automationUpdateInterval: 30, // 30 seconds 98 | raffleEntranceFee: 0.01 ether, 99 | callbackGasLimit: 500000, // 500,000 gas 100 | vrfCoordinatorV2: address(vrfCoordinatorV2Mock), 101 | link: address(link), 102 | deployerKey: DEFAULT_ANVIL_PRIVATE_KEY 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /script/Interactions.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | import {Script, console} from "forge-std/Script.sol"; 5 | import {HelperConfig} from "./HelperConfig.s.sol"; 6 | import {Raffle} from "../src/Raffle.sol"; 7 | import {DevOpsTools} from "foundry-devops/src/DevOpsTools.sol"; 8 | import {VRFCoordinatorV2Mock} from "../test/mocks/VRFCoordinatorV2Mock.sol"; 9 | import {LinkToken} from "../test/mocks/LinkToken.sol"; 10 | 11 | contract CreateSubscription is Script { 12 | function createSubscriptionUsingConfig() public returns (uint64) { 13 | HelperConfig helperConfig = new HelperConfig(); 14 | ( 15 | , 16 | , 17 | , 18 | , 19 | , 20 | address vrfCoordinatorV2, 21 | , 22 | uint256 deployerKey 23 | ) = helperConfig.activeNetworkConfig(); 24 | return createSubscription(vrfCoordinatorV2, deployerKey); 25 | } 26 | 27 | function createSubscription( 28 | address vrfCoordinatorV2, 29 | uint256 deployerKey 30 | ) public returns (uint64) { 31 | console.log("Creating subscription on chainId: ", block.chainid); 32 | vm.startBroadcast(deployerKey); 33 | uint64 subId = VRFCoordinatorV2Mock(vrfCoordinatorV2) 34 | .createSubscription(); 35 | vm.stopBroadcast(); 36 | console.log("Your subscription Id is: ", subId); 37 | console.log("Please update the subscriptionId in HelperConfig.s.sol"); 38 | return subId; 39 | } 40 | 41 | function run() external returns (uint64) { 42 | return createSubscriptionUsingConfig(); 43 | } 44 | } 45 | 46 | contract AddConsumer is Script { 47 | function addConsumer( 48 | address contractToAddToVrf, 49 | address vrfCoordinator, 50 | uint64 subId, 51 | uint256 deployerKey 52 | ) public { 53 | console.log("Adding consumer contract: ", contractToAddToVrf); 54 | console.log("Using vrfCoordinator: ", vrfCoordinator); 55 | console.log("On ChainID: ", block.chainid); 56 | vm.startBroadcast(deployerKey); 57 | VRFCoordinatorV2Mock(vrfCoordinator).addConsumer( 58 | subId, 59 | contractToAddToVrf 60 | ); 61 | vm.stopBroadcast(); 62 | } 63 | 64 | function addConsumerUsingConfig(address mostRecentlyDeployed) public { 65 | HelperConfig helperConfig = new HelperConfig(); 66 | ( 67 | uint64 subId, 68 | , 69 | , 70 | , 71 | , 72 | address vrfCoordinatorV2, 73 | , 74 | uint256 deployerKey 75 | ) = helperConfig.activeNetworkConfig(); 76 | addConsumer(mostRecentlyDeployed, vrfCoordinatorV2, subId, deployerKey); 77 | } 78 | 79 | function run() external { 80 | address mostRecentlyDeployed = DevOpsTools.get_most_recent_deployment( 81 | "Raffle", 82 | block.chainid 83 | ); 84 | addConsumerUsingConfig(mostRecentlyDeployed); 85 | } 86 | } 87 | 88 | contract FundSubscription is Script { 89 | uint96 public constant FUND_AMOUNT = 3 ether; 90 | 91 | function fundSubscriptionUsingConfig() public { 92 | HelperConfig helperConfig = new HelperConfig(); 93 | ( 94 | uint64 subId, 95 | , 96 | , 97 | , 98 | , 99 | address vrfCoordinatorV2, 100 | address link, 101 | uint256 deployerKey 102 | ) = helperConfig.activeNetworkConfig(); 103 | fundSubscription(vrfCoordinatorV2, subId, link, deployerKey); 104 | } 105 | 106 | function fundSubscription( 107 | address vrfCoordinatorV2, 108 | uint64 subId, 109 | address link, 110 | uint256 deployerKey 111 | ) public { 112 | console.log("Funding subscription: ", subId); 113 | console.log("Using vrfCoordinator: ", vrfCoordinatorV2); 114 | console.log("On ChainID: ", block.chainid); 115 | if (block.chainid == 31337) { 116 | vm.startBroadcast(deployerKey); 117 | VRFCoordinatorV2Mock(vrfCoordinatorV2).fundSubscription( 118 | subId, 119 | FUND_AMOUNT 120 | ); 121 | vm.stopBroadcast(); 122 | } else { 123 | console.log(LinkToken(link).balanceOf(msg.sender)); 124 | console.log(msg.sender); 125 | console.log(LinkToken(link).balanceOf(address(this))); 126 | console.log(address(this)); 127 | vm.startBroadcast(deployerKey); 128 | LinkToken(link).transferAndCall( 129 | vrfCoordinatorV2, 130 | FUND_AMOUNT, 131 | abi.encode(subId) 132 | ); 133 | vm.stopBroadcast(); 134 | } 135 | } 136 | 137 | function run() external { 138 | fundSubscriptionUsingConfig(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /test/staging/RaffleStagingTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import {DeployRaffle} from "../../script/DeployRaffle.s.sol"; 6 | import {Raffle} from "../../src/Raffle.sol"; 7 | import {HelperConfig} from "../../script/HelperConfig.s.sol"; 8 | import {Test, console} from "forge-std/Test.sol"; 9 | import {Vm} from "forge-std/Vm.sol"; 10 | import {StdCheats} from "forge-std/StdCheats.sol"; 11 | import {VRFCoordinatorV2Mock} from "../mocks/VRFCoordinatorV2Mock.sol"; 12 | import {CreateSubscription} from "../../script/Interactions.s.sol"; 13 | 14 | contract RaffleTest is StdCheats, Test { 15 | /* Errors */ 16 | event RequestedRaffleWinner(uint256 indexed requestId); 17 | event RaffleEnter(address indexed player); 18 | event WinnerPicked(address indexed player); 19 | 20 | Raffle public raffle; 21 | HelperConfig public helperConfig; 22 | 23 | uint64 subscriptionId; 24 | bytes32 gasLane; 25 | uint256 automationUpdateInterval; 26 | uint256 raffleEntranceFee; 27 | uint32 callbackGasLimit; 28 | address vrfCoordinatorV2; 29 | 30 | address public PLAYER = makeAddr("player"); 31 | uint256 public constant STARTING_USER_BALANCE = 10 ether; 32 | 33 | function setUp() external { 34 | DeployRaffle deployer = new DeployRaffle(); 35 | (raffle, helperConfig) = deployer.run(); 36 | vm.deal(PLAYER, STARTING_USER_BALANCE); 37 | 38 | ( 39 | , 40 | gasLane, 41 | automationUpdateInterval, 42 | raffleEntranceFee, 43 | callbackGasLimit, 44 | vrfCoordinatorV2, // link 45 | // deployerKey 46 | , 47 | 48 | ) = helperConfig.activeNetworkConfig(); 49 | } 50 | 51 | ///////////////////////// 52 | // fulfillRandomWords // 53 | //////////////////////// 54 | 55 | modifier raffleEntered() { 56 | vm.prank(PLAYER); 57 | raffle.enterRaffle{value: raffleEntranceFee}(); 58 | vm.warp(block.timestamp + automationUpdateInterval + 1); 59 | vm.roll(block.number + 1); 60 | _; 61 | } 62 | 63 | modifier onlyOnDeployedContracts() { 64 | if (block.number == 31337) { 65 | return; 66 | } 67 | try vm.activeFork() returns (uint256) { 68 | return; 69 | } catch { 70 | _; 71 | } 72 | } 73 | 74 | function testFulfillRandomWordsCanOnlyBeCalledAfterPerformUpkeep() 75 | public 76 | raffleEntered 77 | onlyOnDeployedContracts 78 | { 79 | // Arrange 80 | // Act / Assert 81 | vm.expectRevert("nonexistent request"); 82 | // vm.mockCall could be used here... 83 | VRFCoordinatorV2Mock(vrfCoordinatorV2).fulfillRandomWords( 84 | 0, 85 | address(raffle) 86 | ); 87 | 88 | vm.expectRevert("nonexistent request"); 89 | 90 | VRFCoordinatorV2Mock(vrfCoordinatorV2).fulfillRandomWords( 91 | 1, 92 | address(raffle) 93 | ); 94 | } 95 | 96 | function testFulfillRandomWordsPicksAWinnerResetsAndSendsMoney() 97 | public 98 | raffleEntered 99 | onlyOnDeployedContracts 100 | { 101 | address expectedWinner = address(1); 102 | 103 | // Arrange 104 | uint256 additionalEntrances = 3; 105 | uint256 startingIndex = 1; // We have starting index be 1 so we can start with address(1) and not address(0) 106 | 107 | for ( 108 | uint256 i = startingIndex; 109 | i < startingIndex + additionalEntrances; 110 | i++ 111 | ) { 112 | address player = address(uint160(i)); 113 | hoax(player, 1 ether); // deal 1 eth to the player 114 | raffle.enterRaffle{value: raffleEntranceFee}(); 115 | } 116 | 117 | uint256 startingTimeStamp = raffle.getLastTimeStamp(); 118 | uint256 startingBalance = expectedWinner.balance; 119 | 120 | // Act 121 | vm.recordLogs(); 122 | raffle.performUpkeep(""); // emits requestId 123 | Vm.Log[] memory entries = vm.getRecordedLogs(); 124 | bytes32 requestId = entries[1].topics[1]; // get the requestId from the logs 125 | 126 | VRFCoordinatorV2Mock(vrfCoordinatorV2).fulfillRandomWords( 127 | uint256(requestId), 128 | address(raffle) 129 | ); 130 | 131 | // Assert 132 | address recentWinner = raffle.getRecentWinner(); 133 | Raffle.RaffleState raffleState = raffle.getRaffleState(); 134 | uint256 winnerBalance = recentWinner.balance; 135 | uint256 endingTimeStamp = raffle.getLastTimeStamp(); 136 | uint256 prize = raffleEntranceFee * (additionalEntrances + 1); 137 | 138 | assert(recentWinner == expectedWinner); 139 | assert(uint256(raffleState) == 0); 140 | assert(winnerBalance == startingBalance + prize); 141 | assert(endingTimeStamp > startingTimeStamp); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foundry Smart Contract Lottery 2 | 3 | This is a section of the Cyfrin Foundry Solidity Course. 4 | 5 | - [Foundry Smart Contract Lottery](#foundry-smart-contract-lottery) 6 | - [Getting Started](#getting-started) 7 | - [Requirements](#requirements) 8 | - [Quickstart](#quickstart) 9 | - [Optional Gitpod](#optional-gitpod) 10 | - [Usage](#usage) 11 | - [Start a local node](#start-a-local-node) 12 | - [Deploy](#deploy) 13 | - [Deploy - Other Network](#deploy---other-network) 14 | - [Testing](#testing) 15 | - [Test Coverage](#test-coverage) 16 | - [Deployment to a testnet or mainnet](#deployment-to-a-testnet-or-mainnet) 17 | - [Scripts](#scripts) 18 | - [Estimate gas](#estimate-gas) 19 | - [Formatting](#formatting) 20 | - [Thank you!](#thank-you) 21 | 22 | 23 | # Getting Started 24 | 25 | ## Requirements 26 | 27 | - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 28 | - You'll know you did it right if you can run `git --version` and you see a response like `git version x.x.x` 29 | - [foundry](https://getfoundry.sh/) 30 | - You'll know you did it right if you can run `forge --version` and you see a response like `forge 0.2.0 (816e00b 2023-03-16T00:05:26.396218Z)` 31 | 32 | 33 | ## Quickstart 34 | 35 | ``` 36 | git clone https://github.com/Cyfrin/foundry-smart-contract-lottery-f23 37 | cd foundry-smart-contract-lottery-f23 38 | forge build 39 | ``` 40 | 41 | ### Optional Gitpod 42 | 43 | If you can't or don't want to run and install locally, you can work with this repo in Gitpod. If you do this, you can skip the `clone this repo` part. 44 | 45 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#github.com/Cyfrin/foundry-smart-contract-lottery-f23) 46 | 47 | # Usage 48 | 49 | ## Start a local node 50 | 51 | ``` 52 | make anvil 53 | ``` 54 | 55 | ## Deploy 56 | 57 | This will default to your local node. You need to have it running in another terminal in order for it to deploy. 58 | 59 | ``` 60 | make deploy 61 | ``` 62 | 63 | ## Deploy - Other Network 64 | 65 | [See below](#deployment-to-a-testnet-or-mainnet) 66 | 67 | ## Testing 68 | 69 | We talk about 4 test tiers in the video. 70 | 71 | 1. Unit 72 | 2. Integration 73 | 3. Forked 74 | 4. Staging 75 | 76 | This repo we cover #1 and #3. 77 | 78 | ``` 79 | forge test 80 | ``` 81 | 82 | or 83 | 84 | ``` 85 | forge test --fork-url $SEPOLIA_RPC_URL 86 | ``` 87 | 88 | ### Test Coverage 89 | 90 | ``` 91 | forge coverage 92 | ``` 93 | 94 | 95 | # Deployment to a testnet or mainnet 96 | 97 | 1. Setup environment variables 98 | 99 | You'll want to set your `SEPOLIA_RPC_URL` and `PRIVATE_KEY` as environment variables. You can add them to a `.env` file, similar to what you see in `.env.example`. 100 | 101 | - `PRIVATE_KEY`: The private key of your account (like from [metamask](https://metamask.io/)). **NOTE:** FOR DEVELOPMENT, PLEASE USE A KEY THAT DOESN'T HAVE ANY REAL FUNDS ASSOCIATED WITH IT. 102 | - You can [learn how to export it here](https://metamask.zendesk.com/hc/en-us/articles/360015289632-How-to-Export-an-Account-Private-Key). 103 | - `SEPOLIA_RPC_URL`: This is url of the goerli testnet node you're working with. You can get setup with one for free from [Alchemy](https://alchemy.com/?a=673c802981) 104 | 105 | Optionally, add your `ETHERSCAN_API_KEY` if you want to verify your contract on [Etherscan](https://etherscan.io/). 106 | 107 | 1. Get testnet ETH 108 | 109 | Head over to [faucets.chain.link](https://faucets.chain.link/) and get some tesnet ETH. You should see the ETH show up in your metamask. 110 | 111 | 2. Deploy 112 | 113 | ``` 114 | make deploy ARGS="--network sepolia" 115 | ``` 116 | 117 | This will setup a ChainlinkVRF Subscription for you. If you already have one, update it in the `scripts/HelperConfig.s.sol` file. It will also automatically add your contract as a consumer. 118 | 119 | 3. Register a Chainlink Automation Upkeep 120 | 121 | [You can follow the documentation if you get lost.](https://docs.chain.link/chainlink-automation/compatible-contracts) 122 | 123 | Go to [automation.chain.link](https://automation.chain.link/new) and register a new upkeep. Choose `Custom logic` as your trigger mechanism for automation. Your UI will look something like this once completed: 124 | 125 | ![Automation](./img/automation.png) 126 | 127 | ## Scripts 128 | 129 | After deploy to a testnet or local net, you can run the scripts. 130 | 131 | Using cast deployed locally example: 132 | 133 | ``` 134 | cast send "enterRaffle()" --value 0.1ether --private-key --rpc-url $SEPOLIA_RPC_URL 135 | ``` 136 | 137 | or, to create a ChainlinkVRF Subscription: 138 | 139 | ``` 140 | make createSubscription ARGS="--network sepolia" 141 | ``` 142 | 143 | 144 | ## Estimate gas 145 | 146 | You can estimate how much gas things cost by running: 147 | 148 | ``` 149 | forge snapshot 150 | ``` 151 | 152 | And you'll see and output file called `.gas-snapshot` 153 | 154 | 155 | # Formatting 156 | 157 | 158 | To run code formatting: 159 | ``` 160 | forge fmt 161 | ``` 162 | 163 | 164 | # Thank you! 165 | 166 | If you appreciated this, feel free to follow me or donate! 167 | 168 | ETH/Arbitrum/Optimism/Polygon/etc Address: 0x9680201d9c93d65a3603d2088d125e955c73BD65 169 | 170 | [![Patrick Collins Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/PatrickAlphaC) 171 | [![Patrick Collins YouTube](https://img.shields.io/badge/YouTube-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/channel/UCn-3f8tw_E1jZvhuHatROwA) 172 | [![Patrick Collins Linkedin](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/patrickalphac/) 173 | [![Patrick Collins Medium](https://img.shields.io/badge/Medium-000000?style=for-the-badge&logo=medium&logoColor=white)](https://medium.com/@patrick.collins_58673/) 174 | -------------------------------------------------------------------------------- /src/Raffle.sol: -------------------------------------------------------------------------------- 1 | // Layout of Contract: 2 | // version 3 | // imports 4 | // errors 5 | // interfaces, libraries, contracts 6 | // Type declarations 7 | // State variables 8 | // Events 9 | // Modifiers 10 | // Functions 11 | 12 | // Layout of Functions: 13 | // constructor 14 | // receive function (if exists) 15 | // fallback function (if exists) 16 | // external 17 | // public 18 | // internal 19 | // private 20 | // view & pure functions 21 | 22 | // SPDX-License-Identifier: MIT 23 | 24 | pragma solidity ^0.8.19; 25 | 26 | import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; 27 | import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; 28 | import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/interfaces/AutomationCompatibleInterface.sol"; 29 | 30 | /**@title A sample Raffle Contract 31 | * @author Patrick Collins 32 | * @notice This contract is for creating a sample raffle contract 33 | * @dev This implements the Chainlink VRF Version 2 34 | */ 35 | contract Raffle is VRFConsumerBaseV2, AutomationCompatibleInterface { 36 | /* Errors */ 37 | error Raffle__UpkeepNotNeeded( 38 | uint256 currentBalance, 39 | uint256 numPlayers, 40 | uint256 raffleState 41 | ); 42 | error Raffle__TransferFailed(); 43 | error Raffle__SendMoreToEnterRaffle(); 44 | error Raffle__RaffleNotOpen(); 45 | 46 | /* Type declarations */ 47 | enum RaffleState { 48 | OPEN, 49 | CALCULATING 50 | } 51 | /* State variables */ 52 | // Chainlink VRF Variables 53 | VRFCoordinatorV2Interface private immutable i_vrfCoordinator; 54 | uint64 private immutable i_subscriptionId; 55 | bytes32 private immutable i_gasLane; 56 | uint32 private immutable i_callbackGasLimit; 57 | uint16 private constant REQUEST_CONFIRMATIONS = 3; 58 | uint32 private constant NUM_WORDS = 1; 59 | 60 | // Lottery Variables 61 | uint256 private immutable i_interval; 62 | uint256 private immutable i_entranceFee; 63 | uint256 private s_lastTimeStamp; 64 | address private s_recentWinner; 65 | address payable[] private s_players; 66 | RaffleState private s_raffleState; 67 | 68 | /* Events */ 69 | event RequestedRaffleWinner(uint256 indexed requestId); 70 | event RaffleEnter(address indexed player); 71 | event WinnerPicked(address indexed player); 72 | 73 | /* Functions */ 74 | constructor( 75 | uint64 subscriptionId, 76 | bytes32 gasLane, // keyHash 77 | uint256 interval, 78 | uint256 entranceFee, 79 | uint32 callbackGasLimit, 80 | address vrfCoordinatorV2 81 | ) VRFConsumerBaseV2(vrfCoordinatorV2) { 82 | i_vrfCoordinator = VRFCoordinatorV2Interface(vrfCoordinatorV2); 83 | i_gasLane = gasLane; 84 | i_interval = interval; 85 | i_subscriptionId = subscriptionId; 86 | i_entranceFee = entranceFee; 87 | s_raffleState = RaffleState.OPEN; 88 | s_lastTimeStamp = block.timestamp; 89 | i_callbackGasLimit = callbackGasLimit; 90 | } 91 | 92 | function enterRaffle() public payable { 93 | // require(msg.value >= i_entranceFee, "Not enough value sent"); 94 | // require(s_raffleState == RaffleState.OPEN, "Raffle is not open"); 95 | if (msg.value < i_entranceFee) { 96 | revert Raffle__SendMoreToEnterRaffle(); 97 | } 98 | if (s_raffleState != RaffleState.OPEN) { 99 | revert Raffle__RaffleNotOpen(); 100 | } 101 | s_players.push(payable(msg.sender)); 102 | // Emit an event when we update a dynamic array or mapping 103 | // Named events with the function name reversed 104 | emit RaffleEnter(msg.sender); 105 | } 106 | 107 | /** 108 | * @dev This is the function that the Chainlink Keeper nodes call 109 | * they look for `upkeepNeeded` to return True. 110 | * the following should be true for this to return true: 111 | * 1. The time interval has passed between raffle runs. 112 | * 2. The lottery is open. 113 | * 3. The contract has ETH. 114 | * 4. Implicity, your subscription is funded with LINK. 115 | */ 116 | function checkUpkeep( 117 | bytes memory /* checkData */ 118 | ) 119 | public 120 | view 121 | override 122 | returns (bool upkeepNeeded, bytes memory /* performData */) 123 | { 124 | bool isOpen = RaffleState.OPEN == s_raffleState; 125 | bool timePassed = ((block.timestamp - s_lastTimeStamp) > i_interval); 126 | bool hasPlayers = s_players.length > 0; 127 | bool hasBalance = address(this).balance > 0; 128 | upkeepNeeded = (timePassed && isOpen && hasBalance && hasPlayers); 129 | return (upkeepNeeded, "0x0"); // can we comment this out? 130 | } 131 | 132 | /** 133 | * @dev Once `checkUpkeep` is returning `true`, this function is called 134 | * and it kicks off a Chainlink VRF call to get a random winner. 135 | */ 136 | function performUpkeep(bytes calldata /* performData */) external override { 137 | (bool upkeepNeeded, ) = checkUpkeep(""); 138 | // require(upkeepNeeded, "Upkeep not needed"); 139 | if (!upkeepNeeded) { 140 | revert Raffle__UpkeepNotNeeded( 141 | address(this).balance, 142 | s_players.length, 143 | uint256(s_raffleState) 144 | ); 145 | } 146 | s_raffleState = RaffleState.CALCULATING; 147 | uint256 requestId = i_vrfCoordinator.requestRandomWords( 148 | i_gasLane, 149 | i_subscriptionId, 150 | REQUEST_CONFIRMATIONS, 151 | i_callbackGasLimit, 152 | NUM_WORDS 153 | ); 154 | // Quiz... is this redundant? 155 | emit RequestedRaffleWinner(requestId); 156 | } 157 | 158 | /** 159 | * @dev This is the function that Chainlink VRF node 160 | * calls to send the money to the random winner. 161 | */ 162 | function fulfillRandomWords( 163 | uint256 /* requestId */, 164 | uint256[] memory randomWords 165 | ) internal override { 166 | // s_players size 10 167 | // randomNumber 202 168 | // 202 % 10 ? what's doesn't divide evenly into 202? 169 | // 20 * 10 = 200 170 | // 2 171 | // 202 % 10 = 2 172 | uint256 indexOfWinner = randomWords[0] % s_players.length; 173 | address payable recentWinner = s_players[indexOfWinner]; 174 | s_recentWinner = recentWinner; 175 | s_players = new address payable[](0); 176 | s_raffleState = RaffleState.OPEN; 177 | s_lastTimeStamp = block.timestamp; 178 | emit WinnerPicked(recentWinner); 179 | (bool success, ) = recentWinner.call{value: address(this).balance}(""); 180 | // require(success, "Transfer failed"); 181 | if (!success) { 182 | revert Raffle__TransferFailed(); 183 | } 184 | } 185 | 186 | /** Getter Functions */ 187 | 188 | function getRaffleState() public view returns (RaffleState) { 189 | return s_raffleState; 190 | } 191 | 192 | function getNumWords() public pure returns (uint256) { 193 | return NUM_WORDS; 194 | } 195 | 196 | function getRequestConfirmations() public pure returns (uint256) { 197 | return REQUEST_CONFIRMATIONS; 198 | } 199 | 200 | function getRecentWinner() public view returns (address) { 201 | return s_recentWinner; 202 | } 203 | 204 | function getPlayer(uint256 index) public view returns (address) { 205 | return s_players[index]; 206 | } 207 | 208 | function getLastTimeStamp() public view returns (uint256) { 209 | return s_lastTimeStamp; 210 | } 211 | 212 | function getInterval() public view returns (uint256) { 213 | return i_interval; 214 | } 215 | 216 | function getEntranceFee() public view returns (uint256) { 217 | return i_entranceFee; 218 | } 219 | 220 | function getNumberOfPlayers() public view returns (uint256) { 221 | return s_players.length; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /test/unit/RaffleTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.19; 4 | 5 | import {DeployRaffle} from "../../script/DeployRaffle.s.sol"; 6 | import {Raffle} from "../../src/Raffle.sol"; 7 | import {HelperConfig} from "../../script/HelperConfig.s.sol"; 8 | import {Test, console} from "forge-std/Test.sol"; 9 | import {Vm} from "forge-std/Vm.sol"; 10 | import {StdCheats} from "forge-std/StdCheats.sol"; 11 | import {VRFCoordinatorV2Mock} from "../mocks/VRFCoordinatorV2Mock.sol"; 12 | import {CreateSubscription} from "../../script/Interactions.s.sol"; 13 | 14 | contract RaffleTest is StdCheats, Test { 15 | /* Errors */ 16 | event RequestedRaffleWinner(uint256 indexed requestId); 17 | event RaffleEnter(address indexed player); 18 | event WinnerPicked(address indexed player); 19 | 20 | Raffle public raffle; 21 | HelperConfig public helperConfig; 22 | 23 | uint64 subscriptionId; 24 | bytes32 gasLane; 25 | uint256 automationUpdateInterval; 26 | uint256 raffleEntranceFee; 27 | uint32 callbackGasLimit; 28 | address vrfCoordinatorV2; 29 | 30 | address public PLAYER = makeAddr("player"); 31 | uint256 public constant STARTING_USER_BALANCE = 10 ether; 32 | 33 | function setUp() external { 34 | DeployRaffle deployer = new DeployRaffle(); 35 | (raffle, helperConfig) = deployer.run(); 36 | vm.deal(PLAYER, STARTING_USER_BALANCE); 37 | 38 | ( 39 | , 40 | gasLane, 41 | automationUpdateInterval, 42 | raffleEntranceFee, 43 | callbackGasLimit, 44 | vrfCoordinatorV2, // link 45 | // deployerKey 46 | , 47 | 48 | ) = helperConfig.activeNetworkConfig(); 49 | } 50 | 51 | function testRaffleInitializesInOpenState() public view { 52 | assert(raffle.getRaffleState() == Raffle.RaffleState.OPEN); 53 | } 54 | 55 | ///////////////////////// 56 | // enterRaffle // 57 | ///////////////////////// 58 | 59 | function testRaffleRevertsWHenYouDontPayEnought() public { 60 | // Arrange 61 | vm.prank(PLAYER); 62 | // Act / Assert 63 | vm.expectRevert(Raffle.Raffle__SendMoreToEnterRaffle.selector); 64 | raffle.enterRaffle(); 65 | } 66 | 67 | function testRaffleRecordsPlayerWhenTheyEnter() public { 68 | // Arrange 69 | vm.prank(PLAYER); 70 | // Act 71 | raffle.enterRaffle{value: raffleEntranceFee}(); 72 | // Assert 73 | address playerRecorded = raffle.getPlayer(0); 74 | assert(playerRecorded == PLAYER); 75 | } 76 | 77 | function testEmitsEventOnEntrance() public { 78 | // Arrange 79 | vm.prank(PLAYER); 80 | 81 | // Act / Assert 82 | vm.expectEmit(true, false, false, false, address(raffle)); 83 | emit RaffleEnter(PLAYER); 84 | raffle.enterRaffle{value: raffleEntranceFee}(); 85 | } 86 | 87 | function testDontAllowPlayersToEnterWhileRaffleIsCalculating() public { 88 | // Arrange 89 | vm.prank(PLAYER); 90 | raffle.enterRaffle{value: raffleEntranceFee}(); 91 | vm.warp(block.timestamp + automationUpdateInterval + 1); 92 | vm.roll(block.number + 1); 93 | raffle.performUpkeep(""); 94 | 95 | // Act / Assert 96 | vm.expectRevert(Raffle.Raffle__RaffleNotOpen.selector); 97 | vm.prank(PLAYER); 98 | raffle.enterRaffle{value: raffleEntranceFee}(); 99 | } 100 | 101 | ///////////////////////// 102 | // checkUpkeep // 103 | ///////////////////////// 104 | function testCheckUpkeepReturnsFalseIfItHasNoBalance() public { 105 | // Arrange 106 | vm.warp(block.timestamp + automationUpdateInterval + 1); 107 | vm.roll(block.number + 1); 108 | 109 | // Act 110 | (bool upkeepNeeded, ) = raffle.checkUpkeep(""); 111 | 112 | // Assert 113 | assert(!upkeepNeeded); 114 | } 115 | 116 | function testCheckUpkeepReturnsFalseIfRaffleIsntOpen() public { 117 | // Arrange 118 | vm.prank(PLAYER); 119 | raffle.enterRaffle{value: raffleEntranceFee}(); 120 | vm.warp(block.timestamp + automationUpdateInterval + 1); 121 | vm.roll(block.number + 1); 122 | raffle.performUpkeep(""); 123 | Raffle.RaffleState raffleState = raffle.getRaffleState(); 124 | // Act 125 | (bool upkeepNeeded, ) = raffle.checkUpkeep(""); 126 | // Assert 127 | assert(raffleState == Raffle.RaffleState.CALCULATING); 128 | assert(upkeepNeeded == false); 129 | } 130 | 131 | // Can you implement this? 132 | function testCheckUpkeepReturnsFalseIfEnoughTimeHasntPassed() public {} 133 | 134 | function testCheckUpkeepReturnsTrueWhenParametersGood() public { 135 | // Arrange 136 | vm.prank(PLAYER); 137 | raffle.enterRaffle{value: raffleEntranceFee}(); 138 | vm.warp(block.timestamp + automationUpdateInterval + 1); 139 | vm.roll(block.number + 1); 140 | 141 | // Act 142 | (bool upkeepNeeded, ) = raffle.checkUpkeep(""); 143 | 144 | // Assert 145 | assert(upkeepNeeded); 146 | } 147 | 148 | ///////////////////////// 149 | // performUpkeep // 150 | ///////////////////////// 151 | 152 | function testPerformUpkeepCanOnlyRunIfCheckUpkeepIsTrue() public { 153 | // Arrange 154 | vm.prank(PLAYER); 155 | raffle.enterRaffle{value: raffleEntranceFee}(); 156 | vm.warp(block.timestamp + automationUpdateInterval + 1); 157 | vm.roll(block.number + 1); 158 | 159 | // Act / Assert 160 | // It doesnt revert 161 | raffle.performUpkeep(""); 162 | } 163 | 164 | function testPerformUpkeepRevertsIfCheckUpkeepIsFalse() public { 165 | // Arrange 166 | uint256 currentBalance = 0; 167 | uint256 numPlayers = 0; 168 | Raffle.RaffleState rState = raffle.getRaffleState(); 169 | // Act / Assert 170 | vm.expectRevert( 171 | abi.encodeWithSelector( 172 | Raffle.Raffle__UpkeepNotNeeded.selector, 173 | currentBalance, 174 | numPlayers, 175 | rState 176 | ) 177 | ); 178 | raffle.performUpkeep(""); 179 | } 180 | 181 | function testPerformUpkeepUpdatesRaffleStateAndEmitsRequestId() public { 182 | // Arrange 183 | vm.prank(PLAYER); 184 | raffle.enterRaffle{value: raffleEntranceFee}(); 185 | vm.warp(block.timestamp + automationUpdateInterval + 1); 186 | vm.roll(block.number + 1); 187 | 188 | // Act 189 | vm.recordLogs(); 190 | raffle.performUpkeep(""); // emits requestId 191 | Vm.Log[] memory entries = vm.getRecordedLogs(); 192 | bytes32 requestId = entries[1].topics[1]; 193 | 194 | // Assert 195 | Raffle.RaffleState raffleState = raffle.getRaffleState(); 196 | // requestId = raffle.getLastRequestId(); 197 | assert(uint256(requestId) > 0); 198 | assert(uint(raffleState) == 1); // 0 = open, 1 = calculating 199 | } 200 | 201 | ///////////////////////// 202 | // fulfillRandomWords // 203 | //////////////////////// 204 | 205 | modifier raffleEntered() { 206 | vm.prank(PLAYER); 207 | raffle.enterRaffle{value: raffleEntranceFee}(); 208 | vm.warp(block.timestamp + automationUpdateInterval + 1); 209 | vm.roll(block.number + 1); 210 | _; 211 | } 212 | 213 | modifier skipFork() { 214 | if (block.chainid != 31337) { 215 | return; 216 | } 217 | _; 218 | } 219 | 220 | function testFulfillRandomWordsCanOnlyBeCalledAfterPerformUpkeep() 221 | public 222 | raffleEntered 223 | skipFork 224 | { 225 | // Arrange 226 | // Act / Assert 227 | vm.expectRevert("nonexistent request"); 228 | // vm.mockCall could be used here... 229 | VRFCoordinatorV2Mock(vrfCoordinatorV2).fulfillRandomWords( 230 | 0, 231 | address(raffle) 232 | ); 233 | 234 | vm.expectRevert("nonexistent request"); 235 | 236 | VRFCoordinatorV2Mock(vrfCoordinatorV2).fulfillRandomWords( 237 | 1, 238 | address(raffle) 239 | ); 240 | } 241 | 242 | function testFulfillRandomWordsPicksAWinnerResetsAndSendsMoney() 243 | public 244 | raffleEntered 245 | skipFork 246 | { 247 | address expectedWinner = address(1); 248 | 249 | // Arrange 250 | uint256 additionalEntrances = 3; 251 | uint256 startingIndex = 1; // We have starting index be 1 so we can start with address(1) and not address(0) 252 | 253 | for ( 254 | uint256 i = startingIndex; 255 | i < startingIndex + additionalEntrances; 256 | i++ 257 | ) { 258 | address player = address(uint160(i)); 259 | hoax(player, 1 ether); // deal 1 eth to the player 260 | raffle.enterRaffle{value: raffleEntranceFee}(); 261 | } 262 | 263 | uint256 startingTimeStamp = raffle.getLastTimeStamp(); 264 | uint256 startingBalance = expectedWinner.balance; 265 | 266 | // Act 267 | vm.recordLogs(); 268 | raffle.performUpkeep(""); // emits requestId 269 | Vm.Log[] memory entries = vm.getRecordedLogs(); 270 | bytes32 requestId = entries[1].topics[1]; // get the requestId from the logs 271 | 272 | VRFCoordinatorV2Mock(vrfCoordinatorV2).fulfillRandomWords( 273 | uint256(requestId), 274 | address(raffle) 275 | ); 276 | 277 | // Assert 278 | address recentWinner = raffle.getRecentWinner(); 279 | Raffle.RaffleState raffleState = raffle.getRaffleState(); 280 | uint256 winnerBalance = recentWinner.balance; 281 | uint256 endingTimeStamp = raffle.getLastTimeStamp(); 282 | uint256 prize = raffleEntranceFee * (additionalEntrances + 1); 283 | 284 | assert(recentWinner == expectedWinner); 285 | assert(uint256(raffleState) == 0); 286 | assert(winnerBalance == startingBalance + prize); 287 | assert(endingTimeStamp > startingTimeStamp); 288 | } 289 | } 290 | --------------------------------------------------------------------------------