Compare commits

...

7 Commits

Author SHA1 Message Date
han
b6fb56fd2c add lottery the first half course 2025-01-04 17:00:53 +07:00
han
938ea14d44 remove Counter contract and add basic Raffle contract 2025-01-03 10:34:36 +07:00
han
6ec32143b7 forge install: forge-std
v1.9.5
2025-01-02 17:26:07 +07:00
han
75f0ce62f8 chore: forge init 2025-01-02 17:26:05 +07:00
han
0a8970589f update lottery dependencies 2025-01-02 17:21:05 +07:00
han
fbabe926ed modules 2025-01-02 17:14:40 +07:00
han
a9bf1f8fe6 add lottery smart contract from updraft 2025-01-02 17:13:18 +07:00
31 changed files with 1660 additions and 0 deletions

21
.gitmodules vendored
View File

@ -13,3 +13,24 @@
[submodule "fund-me/lib/foundry-devops"]
path = fund-me/lib/foundry-devops
url = https://github.com/Cyfrin/foundry-devops
[submodule "foundry-smart-contract-lottery-cu/lib/foundry-devops"]
path = foundry-smart-contract-lottery-cu/lib/foundry-devops
url = https://github.com/cyfrin/foundry-devops
[submodule "foundry-smart-contract-lottery-cu/lib/chainlink-brownie-contracts"]
path = foundry-smart-contract-lottery-cu/lib/chainlink-brownie-contracts
url = https://github.com/smartcontractkit/chainlink-brownie-contracts
[submodule "foundry-smart-contract-lottery-cu/lib/forge-std"]
path = foundry-smart-contract-lottery-cu/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "foundry-smart-contract-lottery-cu/lib/solmate"]
path = foundry-smart-contract-lottery-cu/lib/solmate
url = https://github.com/transmissions11/solmate
[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

View File

@ -0,0 +1,3 @@
PRIVATE_KEY=XXXXXXXXX
RPC_URL=http://0.0.0.0:8545
ETHERSCAN_API_KEY=XXXX

View File

@ -0,0 +1,34 @@
name: test
on: workflow_dispatch
env:
FOUNDRY_PROFILE: ci
jobs:
check:
strategy:
fail-fast: true
name: Foundry project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Run Forge build
run: |
forge --version
forge build --sizes
id: build
- name: Run Forge tests
run: |
forge test -vvv
id: test

View File

@ -0,0 +1,18 @@
# Compiler files
cache/
out/
# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/
/broadcast/
# Docs
docs/
# Dotenv file
.env
# zkSync
zkout/

View File

View File

@ -0,0 +1,6 @@
tasks:
- name: Install Foundry
init: |
curl -L https://foundry.paradigm.xyz | bash
source /home/gitpod/.bashrc
foundryup

View File

@ -0,0 +1,54 @@
-include .env
.PHONY: all test clean deploy fund help install snapshot format anvil
DEFAULT_ANVIL_KEY := 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
help:
@echo "Usage:"
@echo " make deploy [ARGS=...]\n example: make deploy ARGS=\"--network sepolia\""
@echo ""
@echo " make fund [ARGS=...]\n example: make deploy ARGS=\"--network sepolia\""
all: clean remove install update build
# Clean the repo
clean :; forge clean
# Remove modules
remove :; rm -rf .gitmodules && rm -rf .git/modules/* && rm -rf lib && touch .gitmodules && git add . && git commit -m "modules"
install :; forge install cyfrin/foundry-devops@0.2.2 --no-commit && forge install smartcontractkit/chainlink-brownie-contracts@1.1.1 --no-commit && forge install foundry-rs/forge-std@v1.8.2 --no-commit && forge install transmissions11/solmate@v6 --no-commit
# Update Dependencies
update:; forge update
build:; forge build
test :; forge test
snapshot :; forge snapshot
format :; forge fmt
anvil :; anvil -m 'test test test test test test test test test test test junk' --steps-tracing --block-time 1
NETWORK_ARGS := --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast
ifeq ($(findstring --network sepolia,$(ARGS)),--network sepolia)
NETWORK_ARGS := --rpc-url $(SEPOLIA_RPC_URL) --private-key $(PRIVATE_KEY) --broadcast --verify --etherscan-api-key $(ETHERSCAN_API_KEY) -vvvv
endif
deploy:
@forge script script/DeployRaffle.s.sol:DeployRaffle $(NETWORK_ARGS)
createSubscription:
@forge script script/Interactions.s.sol:CreateSubscription $(NETWORK_ARGS)
addConsumer:
@forge script script/Interactions.s.sol:AddConsumer $(NETWORK_ARGS)
fundSubscription:
@forge script script/Interactions.s.sol:FundSubscription $(NETWORK_ARGS)

View File

@ -0,0 +1,204 @@
> ! Updates from Video
> 1. V2.5 of Chainlink VRF uses a `uint256` as a subId instead of a `uint64` this repo has a comment to reflect that. We added a mock in case you'd like to work with version 2.5.
> 2. We use `0.1.0` of the `foundry-devops` package which doesn't need to have `ffi=true`
# Foundry Smart Contract Lottery
This is a section of the Cyfrin Foundry Solidity Course.
*[⭐️ (3:04:09) | Lesson 9: Foundry Smart Contract Lottery](https://www.youtube.com/watch?v=sas02qSFZ74&t=11049s)*
- [Foundry Smart Contract Lottery](#foundry-smart-contract-lottery)
- [Getting Started](#getting-started)
- [Requirements](#requirements)
- [Quickstart](#quickstart)
- [Optional Gitpod](#optional-gitpod)
- [Usage](#usage)
- [Start a local node](#start-a-local-node)
- [Library](#library)
- [Deploy](#deploy)
- [Deploy - Other Network](#deploy---other-network)
- [Testing](#testing)
- [Test Coverage](#test-coverage)
- [Deployment to a testnet or mainnet](#deployment-to-a-testnet-or-mainnet)
- [Scripts](#scripts)
- [Estimate gas](#estimate-gas)
- [Formatting](#formatting)
- [Additional Info:](#additional-info)
- [Let's talk about what "Official" means](#lets-talk-about-what-official-means)
- [Summary](#summary)
- [Thank you!](#thank-you)
# Getting Started
## Requirements
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- You'll know you did it right if you can run `git --version` and you see a response like `git version x.x.x`
- [foundry](https://getfoundry.sh/)
- 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)`
## Quickstart
```
git clone https://github.com/Cyfrin/foundry-smart-contract-lottery-cu
cd foundry-smart-contract-lottery-cu
forge build
```
### Optional Gitpod
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.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#github.com/Cyfrin/foundry-smart-contract-lottery-cu)
# Usage
## Start a local node
```
make anvil
```
## Library
If you're having a hard time installing the chainlink library, you can optionally run this command.
```
forge install smartcontractkit/chainlink-brownie-contracts@0.6.1 --no-commit
```
## Deploy
This will default to your local node. You need to have it running in another terminal in order for it to deploy.
```
make deploy
```
## Deploy - Other Network
[See below](#deployment-to-a-testnet-or-mainnet)
## Testing
We talk about 4 test tiers in the video.
1. Unit
2. Integration
3. Forked
4. Staging
This repo we cover #1 and #3.
```
forge test
```
or
```
forge test --fork-url $SEPOLIA_RPC_URL
```
### Test Coverage
```
forge coverage
```
# Deployment to a testnet or mainnet
1. Setup environment variables
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`.
- `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.
- You can [learn how to export it here](https://metamask.zendesk.com/hc/en-us/articles/360015289632-How-to-Export-an-Account-Private-Key).
- `SEPOLIA_RPC_URL`: This is url of the sepolia testnet node you're working with. You can get setup with one for free from [Alchemy](https://alchemy.com/?a=673c802981)
Optionally, add your `ETHERSCAN_API_KEY` if you want to verify your contract on [Etherscan](https://etherscan.io/).
1. Get testnet ETH
Head over to [faucets.chain.link](https://faucets.chain.link/) and get some testnet ETH. You should see the ETH show up in your metamask.
2. Deploy
```
make deploy ARGS="--network sepolia"
```
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.
3. Register a Chainlink Automation Upkeep
[You can follow the documentation if you get lost.](https://docs.chain.link/chainlink-automation/compatible-contracts)
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:
![Automation](./img/automation.png)
## Scripts
After deploying to a testnet or local net, you can run the scripts.
Using cast deployed locally example:
```
cast send <RAFFLE_CONTRACT_ADDRESS> "enterRaffle()" --value 0.1ether --private-key <PRIVATE_KEY> --rpc-url $SEPOLIA_RPC_URL
```
or, to create a ChainlinkVRF Subscription:
```
make createSubscription ARGS="--network sepolia"
```
## Estimate gas
You can estimate how much gas things cost by running:
```
forge snapshot
```
And you'll see an output file called `.gas-snapshot`
# Formatting
To run code formatting:
```
forge fmt
```
# Additional Info:
Some users were having a confusion that whether Chainlink-brownie-contracts is an official Chainlink repository or not. Here is the info.
Chainlink-brownie-contracts is an official repo. The repository is owned and maintained by the chainlink team for this very purpose, and gets releases from the proper chainlink release process. You can see it's still the `smartcontractkit` org as well.
https://github.com/smartcontractkit/chainlink-brownie-contracts
## Let's talk about what "Official" means
The "official" release process is that chainlink deploys it's packages to [npm](https://www.npmjs.com/package/@chainlink/contracts). So technically, even downloading directly from `smartcontractkit/chainlink` is wrong, because it could be using unreleased code.
So, then you have two options:
1. Download from NPM and have your codebase have dependencies foreign to foundry
2. Download from the chainlink-brownie-contracts repo which already downloads from npm and then packages it nicely for you to use in foundry.
## Summary
1. That is an official repo maintained by the same org
2. It downloads from the official release cycle `chainlink/contracts` use (npm) and packages it nicely for digestion from foundry.
# Thank you!
If you appreciated this, feel free to follow me or donate!
ETH/Arbitrum/Optimism/Polygon/etc Address: 0x9680201d9c93d65a3603d2088d125e955c73BD65
[![Patrick Collins Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/PatrickAlphaC)
[![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)
[![Patrick Collins Linkedin](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/patrickalphac/)
[![Patrick Collins Medium](https://img.shields.io/badge/Medium-000000?style=for-the-badge&logo=medium&logoColor=white)](https://medium.com/@patrick.collins_58673/)

View File

@ -0,0 +1,14 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = ['@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/', '@solmate=lib/solmate/src/']
[etherscan]
mainnet = { key = "${ETHERSCAN_API_KEY}" }
sepolia = {key = "${ETHERSCAN_API_KEY}"}
[rpc_endpoints]
sepolia = "${SEPOLIA_RPC_URL}"
# See more config options https://github.com/foundry-rs/foundry/tree/master/config

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

@ -0,0 +1 @@
Subproject commit 12393bd475bd60c222ff12e75c0f68effe1bbaaf

@ -0,0 +1 @@
Subproject commit 978ac6fadb62f5f0b723c996f64be52eddba6801

@ -0,0 +1 @@
Subproject commit df9f90b490423578142b5dd50752db9427efb2ac

@ -0,0 +1 @@
Subproject commit a9e3ea26a2dc73bfa87f0cb189687d029028e0c5

View File

@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Script} from "forge-std/Script.sol";
import {HelperConfig} from "./HelperConfig.s.sol";
import {Raffle} from "../src/Raffle.sol";
import {AddConsumer, CreateSubscription, FundSubscription} from "./Interactions.s.sol";
contract DeployRaffle is Script {
function run() external returns (Raffle, HelperConfig) {
HelperConfig helperConfig = new HelperConfig(); // This comes with our mocks!
AddConsumer addConsumer = new AddConsumer();
HelperConfig.NetworkConfig memory config = helperConfig.getConfig();
if (config.subscriptionId == 0) {
CreateSubscription createSubscription = new CreateSubscription();
(config.subscriptionId, config.vrfCoordinatorV2_5) =
createSubscription.createSubscription(config.vrfCoordinatorV2_5, config.account);
FundSubscription fundSubscription = new FundSubscription();
fundSubscription.fundSubscription(
config.vrfCoordinatorV2_5, config.subscriptionId, config.link, config.account
);
helperConfig.setConfig(block.chainid, config);
}
vm.startBroadcast(config.account);
Raffle raffle = new Raffle(
config.subscriptionId,
config.gasLane,
config.automationUpdateInterval,
config.raffleEntranceFee,
config.callbackGasLimit,
config.vrfCoordinatorV2_5
);
vm.stopBroadcast();
// We already have a broadcast in here
addConsumer.addConsumer(address(raffle), config.vrfCoordinatorV2_5, config.subscriptionId, config.account);
return (raffle, helperConfig);
}
}

View File

@ -0,0 +1,129 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {LinkToken} from "../test/mocks/LinkToken.sol";
import {Script, console2} from "forge-std/Script.sol";
import {VRFCoordinatorV2_5Mock} from "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol";
abstract contract CodeConstants {
uint96 public MOCK_BASE_FEE = 0.25 ether;
uint96 public MOCK_GAS_PRICE_LINK = 1e9;
// LINK / ETH price
int256 public MOCK_WEI_PER_UINT_LINK = 4e15;
address public FOUNDRY_DEFAULT_SENDER = 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38;
uint256 public constant ETH_SEPOLIA_CHAIN_ID = 11155111;
uint256 public constant ETH_MAINNET_CHAIN_ID = 1;
uint256 public constant LOCAL_CHAIN_ID = 31337;
}
contract HelperConfig is CodeConstants, Script {
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error HelperConfig__InvalidChainId();
/*//////////////////////////////////////////////////////////////
TYPES
//////////////////////////////////////////////////////////////*/
struct NetworkConfig {
uint256 subscriptionId;
bytes32 gasLane;
uint256 automationUpdateInterval;
uint256 raffleEntranceFee;
uint32 callbackGasLimit;
address vrfCoordinatorV2_5;
address link;
address account;
}
/*//////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////*/
// Local network state variables
NetworkConfig public localNetworkConfig;
mapping(uint256 chainId => NetworkConfig) public networkConfigs;
/*//////////////////////////////////////////////////////////////
FUNCTIONS
//////////////////////////////////////////////////////////////*/
constructor() {
networkConfigs[ETH_SEPOLIA_CHAIN_ID] = getSepoliaEthConfig();
networkConfigs[ETH_MAINNET_CHAIN_ID] = getMainnetEthConfig();
// Note: We skip doing the local config
}
function getConfig() public returns (NetworkConfig memory) {
return getConfigByChainId(block.chainid);
}
function setConfig(uint256 chainId, NetworkConfig memory networkConfig) public {
networkConfigs[chainId] = networkConfig;
}
function getConfigByChainId(uint256 chainId) public returns (NetworkConfig memory) {
if (networkConfigs[chainId].vrfCoordinatorV2_5 != address(0)) {
return networkConfigs[chainId];
} else if (chainId == LOCAL_CHAIN_ID) {
return getOrCreateAnvilEthConfig();
} else {
revert HelperConfig__InvalidChainId();
}
}
function getMainnetEthConfig() public pure returns (NetworkConfig memory mainnetNetworkConfig) {
mainnetNetworkConfig = NetworkConfig({
subscriptionId: 0, // If left as 0, our scripts will create one!
gasLane: 0x9fe0eebf5e446e3c998ec9bb19951541aee00bb90ea201ae456421a2ded86805,
automationUpdateInterval: 30, // 30 seconds
raffleEntranceFee: 0.01 ether,
callbackGasLimit: 500000, // 500,000 gas
vrfCoordinatorV2_5: 0x271682DEB8C4E0901D1a1550aD2e64D568E69909,
link: 0x514910771AF9Ca656af840dff83E8264EcF986CA,
account: 0x643315C9Be056cDEA171F4e7b2222a4ddaB9F88D
});
}
function getSepoliaEthConfig() public pure returns (NetworkConfig memory sepoliaNetworkConfig) {
sepoliaNetworkConfig = NetworkConfig({
subscriptionId: 0, // If left as 0, our scripts will create one!
gasLane: 0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae,
automationUpdateInterval: 30, // 30 seconds
raffleEntranceFee: 0.01 ether,
callbackGasLimit: 500000, // 500,000 gas
vrfCoordinatorV2_5: 0x9DdfaCa8183c41ad55329BdeeD9F6A8d53168B1B,
link: 0x779877A7B0D9E8603169DdbD7836e478b4624789,
account: 0x643315C9Be056cDEA171F4e7b2222a4ddaB9F88D
});
}
function getOrCreateAnvilEthConfig() public returns (NetworkConfig memory) {
// Check to see if we set an active network config
if (localNetworkConfig.vrfCoordinatorV2_5 != address(0)) {
return localNetworkConfig;
}
console2.log(unicode"⚠️ You have deployed a mock conract!");
console2.log("Make sure this was intentional");
vm.startBroadcast();
VRFCoordinatorV2_5Mock vrfCoordinatorV2_5Mock =
new VRFCoordinatorV2_5Mock(MOCK_BASE_FEE, MOCK_GAS_PRICE_LINK, MOCK_WEI_PER_UINT_LINK);
LinkToken link = new LinkToken();
uint256 subscriptionId = vrfCoordinatorV2_5Mock.createSubscription();
vm.stopBroadcast();
localNetworkConfig = NetworkConfig({
subscriptionId: subscriptionId,
gasLane: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c, // doesn't really matter
automationUpdateInterval: 30, // 30 seconds
raffleEntranceFee: 0.01 ether,
callbackGasLimit: 500000, // 500,000 gas
vrfCoordinatorV2_5: address(vrfCoordinatorV2_5Mock),
link: address(link),
account: FOUNDRY_DEFAULT_SENDER
});
vm.deal(localNetworkConfig.account, 100 ether);
return localNetworkConfig;
}
}

View File

@ -0,0 +1,103 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Script, console} from "forge-std/Script.sol";
import {HelperConfig} from "./HelperConfig.s.sol";
import {Raffle} from "../src/Raffle.sol";
import {DevOpsTools} from "foundry-devops/src/DevOpsTools.sol";
import {VRFCoordinatorV2_5Mock} from "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol";
import {LinkToken} from "../test/mocks/LinkToken.sol";
import {CodeConstants} from "./HelperConfig.s.sol";
contract CreateSubscription is Script {
function createSubscriptionUsingConfig() public returns (uint256, address) {
HelperConfig helperConfig = new HelperConfig();
address vrfCoordinatorV2_5 = helperConfig.getConfigByChainId(block.chainid).vrfCoordinatorV2_5;
address account = helperConfig.getConfigByChainId(block.chainid).account;
return createSubscription(vrfCoordinatorV2_5, account);
}
function createSubscription(address vrfCoordinatorV2_5, address account) public returns (uint256, address) {
console.log("Creating subscription on chainId: ", block.chainid);
vm.startBroadcast(account);
uint256 subId = VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).createSubscription();
vm.stopBroadcast();
console.log("Your subscription Id is: ", subId);
console.log("Please update the subscriptionId in HelperConfig.s.sol");
return (subId, vrfCoordinatorV2_5);
}
function run() external returns (uint256, address) {
return createSubscriptionUsingConfig();
}
}
contract AddConsumer is Script {
function addConsumer(address contractToAddToVrf, address vrfCoordinator, uint256 subId, address account) public {
console.log("Adding consumer contract: ", contractToAddToVrf);
console.log("Using vrfCoordinator: ", vrfCoordinator);
console.log("On ChainID: ", block.chainid);
vm.startBroadcast(account);
VRFCoordinatorV2_5Mock(vrfCoordinator).addConsumer(subId, contractToAddToVrf);
vm.stopBroadcast();
}
function addConsumerUsingConfig(address mostRecentlyDeployed) public {
HelperConfig helperConfig = new HelperConfig();
uint256 subId = helperConfig.getConfig().subscriptionId;
address vrfCoordinatorV2_5 = helperConfig.getConfig().vrfCoordinatorV2_5;
address account = helperConfig.getConfig().account;
addConsumer(mostRecentlyDeployed, vrfCoordinatorV2_5, subId, account);
}
function run() external {
address mostRecentlyDeployed = DevOpsTools.get_most_recent_deployment("Raffle", block.chainid);
addConsumerUsingConfig(mostRecentlyDeployed);
}
}
contract FundSubscription is CodeConstants, Script {
uint96 public constant FUND_AMOUNT = 3 ether;
function fundSubscriptionUsingConfig() public {
HelperConfig helperConfig = new HelperConfig();
uint256 subId = helperConfig.getConfig().subscriptionId;
address vrfCoordinatorV2_5 = helperConfig.getConfig().vrfCoordinatorV2_5;
address link = helperConfig.getConfig().link;
address account = helperConfig.getConfig().account;
if (subId == 0) {
CreateSubscription createSub = new CreateSubscription();
(uint256 updatedSubId, address updatedVRFv2) = createSub.run();
subId = updatedSubId;
vrfCoordinatorV2_5 = updatedVRFv2;
console.log("New SubId Created! ", subId, "VRF Address: ", vrfCoordinatorV2_5);
}
fundSubscription(vrfCoordinatorV2_5, subId, link, account);
}
function fundSubscription(address vrfCoordinatorV2_5, uint256 subId, address link, address account) public {
console.log("Funding subscription: ", subId);
console.log("Using vrfCoordinator: ", vrfCoordinatorV2_5);
console.log("On ChainID: ", block.chainid);
if (block.chainid == LOCAL_CHAIN_ID) {
vm.startBroadcast(account);
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fundSubscription(subId, FUND_AMOUNT);
vm.stopBroadcast();
} else {
console.log(LinkToken(link).balanceOf(msg.sender));
console.log(msg.sender);
console.log(LinkToken(link).balanceOf(address(this)));
console.log(address(this));
vm.startBroadcast(account);
LinkToken(link).transferAndCall(vrfCoordinatorV2_5, FUND_AMOUNT, abi.encode(subId));
vm.stopBroadcast();
}
}
function run() external {
fundSubscriptionUsingConfig();
}
}

View File

@ -0,0 +1,224 @@
// Layout of Contract:
// version
// imports
// errors
// interfaces, libraries, contracts
// Type declarations
// State variables
// Events
// Modifiers
// Functions
// Layout of Functions:
// constructor
// receive function (if exists)
// fallback function (if exists)
// external
// public
// internal
// private
// view & pure functions
// SPDX-License-Identifier: MIT
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";
import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/interfaces/AutomationCompatibleInterface.sol";
/**
* @title A sample Raffle Contract
* @author Patrick Collins
* @notice This contract is for creating a sample raffle contract
* @dev This implements the Chainlink VRF Version 2
*/
contract Raffle is VRFConsumerBaseV2Plus, AutomationCompatibleInterface {
/* Errors */
error Raffle__UpkeepNotNeeded(uint256 currentBalance, uint256 numPlayers, uint256 raffleState);
error Raffle__TransferFailed();
error Raffle__SendMoreToEnterRaffle();
error Raffle__RaffleNotOpen();
/* Type declarations */
enum RaffleState {
OPEN,
CALCULATING
}
/* State variables */
// Chainlink VRF Variables
uint256 private immutable i_subscriptionId;
bytes32 private immutable i_gasLane;
uint32 private immutable i_callbackGasLimit;
uint16 private constant REQUEST_CONFIRMATIONS = 3;
uint32 private constant NUM_WORDS = 1;
// Lottery Variables
uint256 private immutable i_interval;
uint256 private immutable i_entranceFee;
uint256 private s_lastTimeStamp;
address private s_recentWinner;
address payable[] private s_players;
RaffleState private s_raffleState;
/* Events */
event RequestedRaffleWinner(uint256 indexed requestId);
event RaffleEnter(address indexed player);
event WinnerPicked(address indexed player);
/* Functions */
constructor(
uint256 subscriptionId,
bytes32 gasLane, // keyHash
uint256 interval,
uint256 entranceFee,
uint32 callbackGasLimit,
address vrfCoordinatorV2
) VRFConsumerBaseV2Plus(vrfCoordinatorV2) {
i_gasLane = gasLane;
i_interval = interval;
i_subscriptionId = subscriptionId;
i_entranceFee = entranceFee;
s_raffleState = RaffleState.OPEN;
s_lastTimeStamp = block.timestamp;
i_callbackGasLimit = callbackGasLimit;
// uint256 balance = address(this).balance;
// if (balance > 0) {
// payable(msg.sender).transfer(balance);
// }
}
function enterRaffle() public payable {
// require(msg.value >= i_entranceFee, "Not enough value sent");
// require(s_raffleState == RaffleState.OPEN, "Raffle is not open");
if (msg.value < i_entranceFee) {
revert Raffle__SendMoreToEnterRaffle();
}
if (s_raffleState != RaffleState.OPEN) {
revert Raffle__RaffleNotOpen();
}
s_players.push(payable(msg.sender));
// Emit an event when we update a dynamic array or mapping
// Named events with the function name reversed
emit RaffleEnter(msg.sender);
}
/**
* @dev This is the function that the Chainlink Keeper nodes call
* they look for `upkeepNeeded` to return True.
* the following should be true for this to return true:
* 1. The time interval has passed between raffle runs.
* 2. The lottery is open.
* 3. The contract has ETH.
* 4. Implicity, your subscription is funded with LINK.
*/
function checkUpkeep(bytes memory /* checkData */ )
public
view
override
returns (bool upkeepNeeded, bytes memory /* performData */ )
{
bool isOpen = RaffleState.OPEN == s_raffleState;
bool timePassed = ((block.timestamp - s_lastTimeStamp) > i_interval);
bool hasPlayers = s_players.length > 0;
bool hasBalance = address(this).balance > 0;
upkeepNeeded = (timePassed && isOpen && hasBalance && hasPlayers);
return (upkeepNeeded, "0x0"); // can we comment this out?
}
/**
* @dev Once `checkUpkeep` is returning `true`, this function is called
* and it kicks off a Chainlink VRF call to get a random winner.
*/
function performUpkeep(bytes calldata /* performData */ ) external override {
(bool upkeepNeeded,) = checkUpkeep("");
// require(upkeepNeeded, "Upkeep not needed");
if (!upkeepNeeded) {
revert Raffle__UpkeepNotNeeded(address(this).balance, s_players.length, uint256(s_raffleState));
}
s_raffleState = RaffleState.CALCULATING;
// Will revert if subscription is not set and funded.
uint256 requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: i_gasLane,
subId: i_subscriptionId,
requestConfirmations: REQUEST_CONFIRMATIONS,
callbackGasLimit: i_callbackGasLimit,
numWords: NUM_WORDS,
extraArgs: VRFV2PlusClient._argsToBytes(
// Set nativePayment to true to pay for VRF requests with Sepolia ETH instead of LINK
VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
)
})
);
// Quiz... is this redundant?
emit RequestedRaffleWinner(requestId);
}
/**
* @dev This is the function that Chainlink VRF node
* calls to send the money to the random winner.
*/
function fulfillRandomWords(uint256, /* requestId */ uint256[] calldata randomWords) internal override {
// s_players size 10
// randomNumber 202
// 202 % 10 ? what's doesn't divide evenly into 202?
// 20 * 10 = 200
// 2
// 202 % 10 = 2
uint256 indexOfWinner = randomWords[0] % s_players.length;
address payable recentWinner = s_players[indexOfWinner];
s_recentWinner = recentWinner;
s_players = new address payable[](0);
s_raffleState = RaffleState.OPEN;
s_lastTimeStamp = block.timestamp;
emit WinnerPicked(recentWinner);
(bool success,) = recentWinner.call{value: address(this).balance}("");
// require(success, "Transfer failed");
if (!success) {
revert Raffle__TransferFailed();
}
}
/**
* Getter Functions
*/
function getRaffleState() public view returns (RaffleState) {
return s_raffleState;
}
function getNumWords() public pure returns (uint256) {
return NUM_WORDS;
}
function getRequestConfirmations() public pure returns (uint256) {
return REQUEST_CONFIRMATIONS;
}
function getRecentWinner() public view returns (address) {
return s_recentWinner;
}
function getPlayer(uint256 index) public view returns (address) {
return s_players[index];
}
function getLastTimeStamp() public view returns (uint256) {
return s_lastTimeStamp;
}
function getInterval() public view returns (uint256) {
return i_interval;
}
function getEntranceFee() public view returns (uint256) {
return i_entranceFee;
}
function getNumberOfPlayers() public view returns (uint256) {
return s_players.length;
}
}

View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract ExampleEvents {
uint256 favoriteNumber;
event storedNumber(uint256 indexed oldNumber, uint256 indexed newNumber, uint256 addedNumber, address sender);
function store(uint256 _favoriteNumber) public {
emit storedNumber(favoriteNumber, _favoriteNumber, _favoriteNumber + favoriteNumber, msg.sender);
favoriteNumber = _favoriteNumber;
}
function retrieve() public view returns (uint256) {
return favoriteNumber;
}
}

View File

@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract ExampleModulo {
function getModTen(uint256 number) external pure returns (uint256) {
// 10 % 10 = 0
// 10 % 9 = 1 (10 / 9 = 1.??)
// 2 % 2 = 0. 3 % 2 = 1. 6 % 2 = 0. 7 % 2 = 1
return number % 10;
}
function getModTwo(uint256 number) external pure returns (uint256) {
return number % 2;
}
}

View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract ExampleRevert {
error ExampleRevert__Error();
function revertWithError() public pure {
if (false) {
revert ExampleRevert__Error();
}
}
function revertWithRequire() public pure {
require(true, "ExampleRevert__Error");
}
}

View File

@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
// @dev This contract has been adapted to fit with foundry
pragma solidity ^0.8.0;
import {ERC20} from "@solmate/tokens/ERC20.sol";
interface ERC677Receiver {
function onTokenTransfer(address _sender, uint256 _value, bytes memory _data) external;
}
contract LinkToken is ERC20 {
uint256 constant INITIAL_SUPPLY = 1000000000000000000000000;
uint8 constant DECIMALS = 18;
constructor() ERC20("LinkToken", "LINK", DECIMALS) {
_mint(msg.sender, INITIAL_SUPPLY);
}
function mint(address to, uint256 value) public {
_mint(to, value);
}
event Transfer(address indexed from, address indexed to, uint256 value, bytes data);
/**
* @dev transfer token to a contract address with additional data if the recipient is a contact.
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
* @param _data The extra data to be passed to the receiving contract.
*/
function transferAndCall(address _to, uint256 _value, bytes memory _data) public virtual returns (bool success) {
super.transfer(_to, _value);
// emit Transfer(msg.sender, _to, _value, _data);
emit Transfer(msg.sender, _to, _value, _data);
if (isContract(_to)) {
contractFallback(_to, _value, _data);
}
return true;
}
// PRIVATE
function contractFallback(address _to, uint256 _value, bytes memory _data) private {
ERC677Receiver receiver = ERC677Receiver(_to);
receiver.onTokenTransfer(msg.sender, _value, _data);
}
function isContract(address _addr) private view returns (bool hasCode) {
uint256 length;
assembly {
length := extcodesize(_addr)
}
return length > 0;
}
}

View File

@ -0,0 +1,118 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {DeployRaffle} from "../../script/DeployRaffle.s.sol";
import {Raffle} from "../../src/Raffle.sol";
import {HelperConfig} from "../../script/HelperConfig.s.sol";
import {Test, console} from "forge-std/Test.sol";
import {Vm} from "forge-std/Vm.sol";
import {StdCheats} from "forge-std/StdCheats.sol";
import {VRFCoordinatorV2_5Mock} from "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol";
import {CreateSubscription} from "../../script/Interactions.s.sol";
contract RaffleTest is StdCheats, Test {
/* Errors */
event RequestedRaffleWinner(uint256 indexed requestId);
event RaffleEnter(address indexed player);
event WinnerPicked(address indexed player);
Raffle public raffle;
HelperConfig public helperConfig;
uint256 subscriptionId;
bytes32 gasLane;
uint256 automationUpdateInterval;
uint256 raffleEntranceFee;
uint32 callbackGasLimit;
address vrfCoordinatorV2_5;
address public PLAYER = makeAddr("player");
uint256 public constant STARTING_USER_BALANCE = 10 ether;
function setUp() external {
DeployRaffle deployer = new DeployRaffle();
(raffle, helperConfig) = deployer.run();
vm.deal(PLAYER, STARTING_USER_BALANCE);
HelperConfig.NetworkConfig memory config = helperConfig.getConfig();
subscriptionId = config.subscriptionId;
gasLane = config.gasLane;
automationUpdateInterval = config.automationUpdateInterval;
raffleEntranceFee = config.raffleEntranceFee;
callbackGasLimit = config.callbackGasLimit;
vrfCoordinatorV2_5 = config.vrfCoordinatorV2_5;
}
/////////////////////////
// fulfillRandomWords //
////////////////////////
modifier raffleEntered() {
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
_;
}
modifier onlyOnDeployedContracts() {
if (block.chainid == 31337) {
return;
}
try vm.activeFork() returns (uint256) {
return;
} catch {
_;
}
}
function testFulfillRandomWordsCanOnlyBeCalledAfterPerformUpkeep() public raffleEntered onlyOnDeployedContracts {
// Arrange
// Act / Assert
vm.expectRevert("nonexistent request");
// vm.mockCall could be used here...
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fulfillRandomWords(0, address(raffle));
vm.expectRevert("nonexistent request");
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fulfillRandomWords(1, address(raffle));
}
function testFulfillRandomWordsPicksAWinnerResetsAndSendsMoney() public raffleEntered onlyOnDeployedContracts {
address expectedWinner = address(1);
// Arrange
uint256 additionalEntrances = 3;
uint256 startingIndex = 1; // We have starting index be 1 so we can start with address(1) and not address(0)
for (uint256 i = startingIndex; i < startingIndex + additionalEntrances; i++) {
address player = address(uint160(i));
hoax(player, 1 ether); // deal 1 eth to the player
raffle.enterRaffle{value: raffleEntranceFee}();
}
uint256 startingTimeStamp = raffle.getLastTimeStamp();
uint256 startingBalance = expectedWinner.balance;
// Act
vm.recordLogs();
raffle.performUpkeep(""); // emits requestId
Vm.Log[] memory entries = vm.getRecordedLogs();
bytes32 requestId = entries[1].topics[1]; // get the requestId from the logs
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fulfillRandomWords(uint256(requestId), address(raffle));
// Assert
address recentWinner = raffle.getRecentWinner();
Raffle.RaffleState raffleState = raffle.getRaffleState();
uint256 winnerBalance = recentWinner.balance;
uint256 endingTimeStamp = raffle.getLastTimeStamp();
uint256 prize = raffleEntranceFee * (additionalEntrances + 1);
assert(recentWinner == expectedWinner);
assert(uint256(raffleState) == 0);
assert(winnerBalance == startingBalance + prize);
assert(endingTimeStamp > startingTimeStamp);
}
}

View File

@ -0,0 +1,280 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {DeployRaffle} from "../../script/DeployRaffle.s.sol";
import {Raffle} from "../../src/Raffle.sol";
import {HelperConfig} from "../../script/HelperConfig.s.sol";
import {Test, console2} from "forge-std/Test.sol";
import {Vm} from "forge-std/Vm.sol";
import {VRFCoordinatorV2_5Mock} from "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol";
import {LinkToken} from "../../test/mocks/LinkToken.sol";
import {CodeConstants} from "../../script/HelperConfig.s.sol";
contract RaffleTest is Test, CodeConstants {
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
event RequestedRaffleWinner(uint256 indexed requestId);
event RaffleEnter(address indexed player);
event WinnerPicked(address indexed player);
Raffle public raffle;
HelperConfig public helperConfig;
uint256 subscriptionId;
bytes32 gasLane;
uint256 automationUpdateInterval;
uint256 raffleEntranceFee;
uint32 callbackGasLimit;
address vrfCoordinatorV2_5;
LinkToken link;
address public PLAYER = makeAddr("player");
uint256 public constant STARTING_USER_BALANCE = 10 ether;
uint256 public constant LINK_BALANCE = 100 ether;
function setUp() external {
DeployRaffle deployer = new DeployRaffle();
(raffle, helperConfig) = deployer.run();
vm.deal(PLAYER, STARTING_USER_BALANCE);
HelperConfig.NetworkConfig memory config = helperConfig.getConfig();
subscriptionId = config.subscriptionId;
gasLane = config.gasLane;
automationUpdateInterval = config.automationUpdateInterval;
raffleEntranceFee = config.raffleEntranceFee;
callbackGasLimit = config.callbackGasLimit;
vrfCoordinatorV2_5 = config.vrfCoordinatorV2_5;
link = LinkToken(config.link);
vm.startPrank(msg.sender);
if (block.chainid == LOCAL_CHAIN_ID) {
link.mint(msg.sender, LINK_BALANCE);
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fundSubscription(subscriptionId, LINK_BALANCE);
}
link.approve(vrfCoordinatorV2_5, LINK_BALANCE);
vm.stopPrank();
}
function testRaffleInitializesInOpenState() public view {
assert(raffle.getRaffleState() == Raffle.RaffleState.OPEN);
}
/*//////////////////////////////////////////////////////////////
ENTER RAFFLE
//////////////////////////////////////////////////////////////*/
function testRaffleRevertsWHenYouDontPayEnought() public {
// Arrange
vm.prank(PLAYER);
// Act / Assert
vm.expectRevert(Raffle.Raffle__SendMoreToEnterRaffle.selector);
raffle.enterRaffle();
}
function testRaffleRecordsPlayerWhenTheyEnter() public {
// Arrange
vm.prank(PLAYER);
// Act
raffle.enterRaffle{value: raffleEntranceFee}();
// Assert
address playerRecorded = raffle.getPlayer(0);
assert(playerRecorded == PLAYER);
}
function testEmitsEventOnEntrance() public {
// Arrange
vm.prank(PLAYER);
// Act / Assert
vm.expectEmit(true, false, false, false, address(raffle));
emit RaffleEnter(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
}
function testDontAllowPlayersToEnterWhileRaffleIsCalculating() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
raffle.performUpkeep("");
// Act / Assert
vm.expectRevert(Raffle.Raffle__RaffleNotOpen.selector);
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
}
/*//////////////////////////////////////////////////////////////
CHECKUPKEEP
//////////////////////////////////////////////////////////////*/
function testCheckUpkeepReturnsFalseIfItHasNoBalance() public {
// Arrange
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
// Act
(bool upkeepNeeded,) = raffle.checkUpkeep("");
// Assert
assert(!upkeepNeeded);
}
function testCheckUpkeepReturnsFalseIfRaffleIsntOpen() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
raffle.performUpkeep("");
Raffle.RaffleState raffleState = raffle.getRaffleState();
// Act
(bool upkeepNeeded,) = raffle.checkUpkeep("");
// Assert
assert(raffleState == Raffle.RaffleState.CALCULATING);
assert(upkeepNeeded == false);
}
// Challenge 1. testCheckUpkeepReturnsFalseIfEnoughTimeHasntPassed
function testCheckUpkeepReturnsFalseIfEnoughTimeHasntPassed() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
// Act
(bool upkeepNeeded,) = raffle.checkUpkeep("");
// Assert
assert(!upkeepNeeded);
}
// Challenge 2. testCheckUpkeepReturnsTrueWhenParametersGood
function testCheckUpkeepReturnsTrueWhenParametersGood() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
// Act
(bool upkeepNeeded,) = raffle.checkUpkeep("");
// Assert
assert(upkeepNeeded);
}
/*//////////////////////////////////////////////////////////////
PERFORMUPKEEP
//////////////////////////////////////////////////////////////*/
function testPerformUpkeepCanOnlyRunIfCheckUpkeepIsTrue() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
// Act / Assert
// It doesnt revert
raffle.performUpkeep("");
}
function testPerformUpkeepRevertsIfCheckUpkeepIsFalse() public {
// Arrange
uint256 currentBalance = 0;
uint256 numPlayers = 0;
Raffle.RaffleState rState = raffle.getRaffleState();
// Act / Assert
vm.expectRevert(
abi.encodeWithSelector(Raffle.Raffle__UpkeepNotNeeded.selector, currentBalance, numPlayers, rState)
);
raffle.performUpkeep("");
}
function testPerformUpkeepUpdatesRaffleStateAndEmitsRequestId() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
// Act
vm.recordLogs();
raffle.performUpkeep(""); // emits requestId
Vm.Log[] memory entries = vm.getRecordedLogs();
bytes32 requestId = entries[1].topics[1];
// Assert
Raffle.RaffleState raffleState = raffle.getRaffleState();
// requestId = raffle.getLastRequestId();
assert(uint256(requestId) > 0);
assert(uint256(raffleState) == 1); // 0 = open, 1 = calculating
}
/*//////////////////////////////////////////////////////////////
FULFILLRANDOMWORDS
//////////////////////////////////////////////////////////////*/
modifier raffleEntered() {
vm.prank(PLAYER);
raffle.enterRaffle{value: raffleEntranceFee}();
vm.warp(block.timestamp + automationUpdateInterval + 1);
vm.roll(block.number + 1);
_;
}
modifier skipFork() {
if (block.chainid != 31337) {
return;
}
_;
}
function testFulfillRandomWordsCanOnlyBeCalledAfterPerformUpkeep() public raffleEntered skipFork {
// Arrange
// Act / Assert
vm.expectRevert(VRFCoordinatorV2_5Mock.InvalidRequest.selector);
// vm.mockCall could be used here...
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fulfillRandomWords(0, address(raffle));
vm.expectRevert(VRFCoordinatorV2_5Mock.InvalidRequest.selector);
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fulfillRandomWords(1, address(raffle));
}
function testFulfillRandomWordsPicksAWinnerResetsAndSendsMoney() public raffleEntered skipFork {
address expectedWinner = address(1);
// Arrange
uint256 additionalEntrances = 3;
uint256 startingIndex = 1; // We have starting index be 1 so we can start with address(1) and not address(0)
for (uint256 i = startingIndex; i < startingIndex + additionalEntrances; i++) {
address player = address(uint160(i));
hoax(player, 1 ether); // deal 1 eth to the player
raffle.enterRaffle{value: raffleEntranceFee}();
}
uint256 startingTimeStamp = raffle.getLastTimeStamp();
uint256 startingBalance = expectedWinner.balance;
// Act
vm.recordLogs();
raffle.performUpkeep(""); // emits requestId
Vm.Log[] memory entries = vm.getRecordedLogs();
console2.logBytes32(entries[1].topics[1]);
bytes32 requestId = entries[1].topics[1]; // get the requestId from the logs
VRFCoordinatorV2_5Mock(vrfCoordinatorV2_5).fulfillRandomWords(uint256(requestId), address(raffle));
// Assert
address recentWinner = raffle.getRecentWinner();
Raffle.RaffleState raffleState = raffle.getRaffleState();
uint256 winnerBalance = recentWinner.balance;
uint256 endingTimeStamp = raffle.getLastTimeStamp();
uint256 prize = raffleEntranceFee * (additionalEntrances + 1);
assert(recentWinner == expectedWinner);
assert(uint256(raffleState) == 0);
assert(winnerBalance == startingBalance + prize);
assert(endingTimeStamp > startingTimeStamp);
}
}

43
lottery/.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: CI
on:
push:
pull_request:
workflow_dispatch:
env:
FOUNDRY_PROFILE: ci
jobs:
check:
strategy:
fail-fast: true
name: Foundry project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Show Forge version
run: |
forge --version
- name: Run Forge fmt
run: |
forge fmt --check
id: fmt
- name: Run Forge build
run: |
forge build --sizes
id: build
- name: Run Forge tests
run: |
forge test -vvv
id: test

14
lottery/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
# Compiler files
cache/
out/
# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/
# Docs
docs/
# Dotenv file
.env

66
lottery/README.md Normal file
View File

@ -0,0 +1,66 @@
## Foundry
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
Foundry consists of:
- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
## Documentation
https://book.getfoundry.sh/
## Usage
### Build
```shell
$ forge build
```
### Test
```shell
$ forge test
```
### Format
```shell
$ forge fmt
```
### Gas Snapshots
```shell
$ forge snapshot
```
### Anvil
```shell
$ anvil
```
### Deploy
```shell
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
```
### Cast
```shell
$ cast <subcommand>
```
### Help
```shell
$ forge --help
$ anvil --help
$ cast --help
```

7
lottery/foundry.toml Normal file
View File

@ -0,0 +1,7 @@
[profile.default]
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

@ -0,0 +1 @@
Subproject commit 12393bd475bd60c222ff12e75c0f68effe1bbaaf

1
lottery/lib/forge-std Submodule

@ -0,0 +1 @@
Subproject commit b93cf4bc34ff214c099dc970b153f85ade8c9f66

167
lottery/src/Raffle.sol Normal file
View File

@ -0,0 +1,167 @@
// Layout of Contract:
// version
// imports
// errors
// interfaces, libraries, contracts
// Type declarations
// State variables
// Events
// Modifiers
// Functions
// Layout of Functions:
// constructor
// receive function (if exists)
// fallback function (if exists)
// external
// public
// internal
// private
// view & pure functions
// 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 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;
// @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() 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);
}
/**
* @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;
}
}