From b6fb56fd2ce09340b382f06e13d35921aafdc180 Mon Sep 17 00:00:00 2001 From: han Date: Sat, 4 Jan 2025 17:00:53 +0700 Subject: [PATCH] add lottery the first half course --- .gitmodules | 6 + lottery/foundry.toml | 1 + lottery/lib/chainlink-brownie-contracts | 1 + lottery/src/Raffle.sol | 139 ++++++++++++++++++++++-- 4 files changed, 137 insertions(+), 10 deletions(-) create mode 160000 lottery/lib/chainlink-brownie-contracts diff --git a/.gitmodules b/.gitmodules index 1035188..a808097 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,9 @@ [submodule "lottery/lib/forge-std"] path = lottery/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lottery/lib/chainlink"] + path = lottery/lib/chainlink + url = https://github.com/smartcontractkit/chainlink +[submodule "lottery/lib/chainlink-brownie-contracts"] + path = lottery/lib/chainlink-brownie-contracts + url = https://github.com/smartcontractkit/chainlink-brownie-contracts diff --git a/lottery/foundry.toml b/lottery/foundry.toml index 25b918f..4f698c6 100644 --- a/lottery/foundry.toml +++ b/lottery/foundry.toml @@ -2,5 +2,6 @@ src = "src" out = "out" libs = ["lib"] +remappings = ['@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/'] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lottery/lib/chainlink-brownie-contracts b/lottery/lib/chainlink-brownie-contracts new file mode 160000 index 0000000..12393bd --- /dev/null +++ b/lottery/lib/chainlink-brownie-contracts @@ -0,0 +1 @@ +Subproject commit 12393bd475bd60c222ff12e75c0f68effe1bbaaf diff --git a/lottery/src/Raffle.sol b/lottery/src/Raffle.sol index 5b379dd..4b8d7ac 100644 --- a/lottery/src/Raffle.sol +++ b/lottery/src/Raffle.sol @@ -22,26 +22,145 @@ // SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity 0.8.19; +import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol"; +import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol"; + /** * @title A sample Raffle contract * @author hirugohan * @notice This contract is for creating a sample raffle * @dev Implements Chainlink VRFv2.5 */ -contract Raffle { +contract Raffle is VRFConsumerBaseV2Plus { + /* Errors */ + error Raffle__SendMoreToEnterRaffle(); + error Raffle__TransferFailed(); + error Raffle__RaffleNotOpen(); + error Raffle__UpkeepNotNeeded(uint256 balance, uint256 playersLength, uint256 raffleState); + + /* Type Declarations */ + enum RaffleState { + OPEN, // 0 + CALCULATING // 1 + + } + + /* State Variables */ + uint16 private constant REQUEST_CONFIRMATIONS = 3; + uint32 private constant NUM_WORDS = 1; uint256 private immutable i_entranceFee; - - constructor (uint256 entranceFee) { + // @dev The duration of the lottery in seconds + uint256 private immutable i_interval; + bytes32 private immutable i_keyHash; + uint256 private immutable i_subscriptionId; + uint32 private immutable i_callbackGasLimit; + address payable[] private s_players; + uint256 private s_lastTimeStamp; + address private s_recentWinner; + RaffleState private s_raffleState; + + /* Events */ + event RaffleEntered(address indexed player); + event WinnerPicked(address indexed winner); + + constructor( + uint256 entranceFee, + uint256 interval, + address vrfCoordinator, + bytes32 gasLane, + uint256 subscriptionId, + uint32 callbackGasLimit + ) VRFConsumerBaseV2Plus(vrfCoordinator) { i_entranceFee = entranceFee; + i_interval = interval; + i_keyHash = gasLane; + i_subscriptionId = subscriptionId; + i_callbackGasLimit = callbackGasLimit; + + s_lastTimeStamp = block.timestamp; + s_raffleState = RaffleState.OPEN; } - - function enterRaffle() public payable { - require(msg.value >= i_entranceFee, "Not enough ETH sent!"); + + function enterRaffle() external payable { + // require(msg.value >= i_entranceFee, "Not enough ETH sent!"); + if (msg.value < i_entranceFee) { + revert Raffle__SendMoreToEnterRaffle(); + } + if (s_raffleState != RaffleState.OPEN) { + revert Raffle__RaffleNotOpen(); + } + + // require(msg.value >= i_entranceFee, SendMoreToEnterRaffle()); + s_players.push(payable(msg.sender)); + emit RaffleEntered(msg.sender); } - - function pickWinner() public {} - - /** Getter Functions */ + + /** + * @dev This is a function that chainlink nodes will call to see + * if the lottery is ready to have a winner picked. + * The following should be true in order for upkeepNeeded to be true: + * 1. The time interval has passed between raffle runes + * 2. The lottery is open + * 3. The contract has ETH (has players) + * 4. Implicitly, your subscription has LINK + * @param - ignored + * @return upkeepNeeded - true if it's time to restart the lottery + * @return - ignored + */ + function checkUpkeep(bytes memory /* checkData */ ) + public + view + returns (bool upkeepNeeded, bytes memory /* performData */ ) + { + bool timeHasPassed = ((block.timestamp - s_lastTimeStamp) >= i_interval); + bool isOpen = s_raffleState == RaffleState.OPEN; + bool hasBalance = address(this).balance > 0; + bool hasPlayers = s_players.length > 0; + upkeepNeeded = timeHasPassed && isOpen && hasBalance && hasPlayers; + return (upkeepNeeded, ""); + } + + function performUpkeep(bytes calldata /* performData */) external { + (bool upkeepNeeded, ) = checkUpkeep(""); + if (!upkeepNeeded) { + revert Raffle__UpkeepNotNeeded(address(this).balance, s_players.length, uint256(s_raffleState)); + } + + s_raffleState = RaffleState.CALCULATING; + VRFV2PlusClient.RandomWordsRequest memory request = VRFV2PlusClient.RandomWordsRequest({ + keyHash: i_keyHash, + subId: i_subscriptionId, + requestConfirmations: REQUEST_CONFIRMATIONS, + callbackGasLimit: i_callbackGasLimit, + numWords: NUM_WORDS, + extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false})) + }); + s_vrfCoordinator.requestRandomWords(request); + } + + // CEI: Check, Effects, Interactions Pattern + function fulfillRandomWords(uint256 /* requestId */, uint256[] calldata randomWords) internal virtual override { + // Checks: check values before executing function + + // Effect (Internal Contract State) + uint256 indexOfWinner = randomWords[0] % s_players.length; + address payable recentWinner = s_players[indexOfWinner]; + s_recentWinner = recentWinner; + s_raffleState = RaffleState.OPEN; + s_players = new address payable[](0); + s_lastTimeStamp = block.timestamp; + emit WinnerPicked(s_recentWinner); + + // Interactions (External Contract Interactions) + (bool success,) = recentWinner.call{value: address(this).balance}(""); + if (!success) { + revert Raffle__TransferFailed(); + } + } + + /** + * Getter Functions + */ function getEntranceFee() external view returns (uint256) { return i_entranceFee; }