finish up lottery course

This commit is contained in:
han 2025-01-06 21:19:15 +07:00
parent b6fb56fd2c
commit 54dda02722
12 changed files with 601 additions and 17 deletions

6
.gitmodules vendored
View File

@ -34,3 +34,9 @@
[submodule "lottery/lib/chainlink-brownie-contracts"] [submodule "lottery/lib/chainlink-brownie-contracts"]
path = lottery/lib/chainlink-brownie-contracts path = lottery/lib/chainlink-brownie-contracts
url = https://github.com/smartcontractkit/chainlink-brownie-contracts url = https://github.com/smartcontractkit/chainlink-brownie-contracts
[submodule "lottery/lib/solmate"]
path = lottery/lib/solmate
url = https://github.com/transmissions11/solmate
[submodule "lottery/lib/foundry-devops"]
path = lottery/lib/foundry-devops
url = https://github.com/cyfrin/foundry-devops

12
lottery/Makefile Normal file
View File

@ -0,0 +1,12 @@
-include .env
.PHONY: all test deploy
build :; forge build
test :; forge test
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
deploy-sepolia:
@forge script scripts/DeployRaffle.s.sol:DeployRaffle --rpc-url $(SEPOLIA_RPC_URL) --account default --broadcast --verify --etherscan-api-key $(ETHERSCAN_API_KEY) -vvvv

View File

@ -2,6 +2,16 @@
src = "src" src = "src"
out = "out" out = "out"
libs = ["lib"] libs = ["lib"]
remappings = ['@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/'] remappings = [
'@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/',
'@solmate=lib/solmate/src/'
]
fs_permissions = [
{ access = "read", path = "./broadcast" },
{ access = "read", path = "./reports" },
]
[fuzz]
runs = 256
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

@ -1 +1 @@
Subproject commit b93cf4bc34ff214c099dc970b153f85ade8c9f66 Subproject commit 978ac6fadb62f5f0b723c996f64be52eddba6801

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

1
lottery/lib/solmate Submodule

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

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {Script} from "forge-std/Script.sol";
import {Raffle} from "src/Raffle.sol";
import {HelperConfig} from "script/HelperConfig.s.sol";
import {CreateSubscription, FundSubscription, AddConsumer} from "script/Interactions.s.sol";
contract DeployRaffle is Script {
function run() public {}
function deployContract() public returns(Raffle, HelperConfig) {
HelperConfig helperConfig = new HelperConfig();
// local => deploy mocks, get local config
// sepolia => get sepolia config
HelperConfig.NetworkConfig memory config = helperConfig.getConfig();
if (config.subscriptionId == 0) {
// create subscription
CreateSubscription createSubscription = new CreateSubscription();
(config.subscriptionId, config.vrfCoordinator) = createSubscription.createSubscription(config.vrfCoordinator, config.account);
// fund it!
FundSubscription fundSubscription = new FundSubscription();
fundSubscription.fundSubscription(config.vrfCoordinator, config.subscriptionId, config.link, config.account);
}
vm.startBroadcast(config.account);
Raffle raffle = new Raffle(
config.entranceFee,
config.interval,
config.vrfCoordinator,
config.gasLane,
config.subscriptionId,
config.callbackGasLimit
);
vm.stopBroadcast();
AddConsumer addConsumer = new AddConsumer();
addConsumer.addConsumer(address(raffle), config.vrfCoordinator, config.subscriptionId, config.account);
return (raffle, helperConfig);
}
}

View File

@ -0,0 +1,93 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {Script} from "forge-std/Script.sol";
import {VRFCoordinatorV2_5Mock} from "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol";
import {LinkToken} from "test/mocks/LinkToken.sol";
abstract contract CodeConstants {
/* VRF Mock Values */
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;
uint256 public constant ETH_SEPOLIA_CHAIN_ID = 11155111;
uint256 public constant LOCAL_CHAIN_ID = 31337;
}
contract HelperConfig is CodeConstants, Script {
error HelperConfig__InvalidChainId();
struct NetworkConfig {
uint256 entranceFee;
uint256 interval;
address vrfCoordinator;
bytes32 gasLane;
uint32 callbackGasLimit;
uint256 subscriptionId;
address link;
address account;
}
NetworkConfig public localNetworkConfig;
mapping(uint256 chainid => NetworkConfig) public networkConfigs;
constructor() {
networkConfigs[ETH_SEPOLIA_CHAIN_ID] = getSepoliaEthConfig();
}
function getConfigByChainId(uint256 chainId) public returns (NetworkConfig memory) {
if (networkConfigs[chainId].vrfCoordinator != address(0)) {
return networkConfigs[chainId];
} else if (chainId == LOCAL_CHAIN_ID) {
// get or create anvil eth config
return getOrCreateAnvilEthConfig();
} else {
revert HelperConfig__InvalidChainId();
}
}
function getConfig() public returns (NetworkConfig memory) {
return getConfigByChainId(block.chainid);
}
function getSepoliaEthConfig() public pure returns (NetworkConfig memory) {
return NetworkConfig({
entranceFee: 0.01 ether, // 1e16
interval: 30, // 30 seconds
vrfCoordinator: 0x9DdfaCa8183c41ad55329BdeeD9F6A8d53168B1B,
gasLane: 0x130dba50ad435d4ecc214aad0d5820474137bd68e7e77724144f27c3c377d3d4,
callbackGasLimit: 500000, // 500,000 gas
subscriptionId: 0,
link: 0x779877A7B0D9E8603169DdbD7836e478b4624789,
account: address(1)
});
}
function getOrCreateAnvilEthConfig() public returns (NetworkConfig memory) {
// check if already set network config
if (localNetworkConfig.vrfCoordinator != address(0)) {
return localNetworkConfig;
}
// deploy mocks and such
vm.startBroadcast();
VRFCoordinatorV2_5Mock vrfCoordinatorMock = new VRFCoordinatorV2_5Mock(MOCK_BASE_FEE, MOCK_GAS_PRICE_LINK, MOCK_WEI_PER_UINT_LINK);
LinkToken linkToken = new LinkToken();
vm.stopBroadcast();
localNetworkConfig = NetworkConfig({
entranceFee: 0.01 ether, // 1e16
interval: 30, // 30 seconds
vrfCoordinator: address(vrfCoordinatorMock),
// doesn't matter
gasLane: 0x130dba50ad435d4ecc214aad0d5820474137bd68e7e77724144f27c3c377d3d4,
callbackGasLimit: 500000, // 500,000 gas
subscriptionId: 0, // might have to fix
link: address(linkToken),
account: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
});
return localNetworkConfig;
}
}

View File

@ -0,0 +1,92 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {Script, console} from "forge-std/Script.sol";
import {HelperConfig, CodeConstants} from "script/HelperConfig.s.sol";
import {VRFCoordinatorV2_5Mock} from "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol";
import {LinkToken} from "test/mocks/LinkToken.sol";
import {DevOpsTools} from "lib/foundry-devops/src/DevOpsTools.sol";
contract CreateSubscription is Script {
function createSubscriptionUsingConfig() public returns (uint256, address) {
HelperConfig helperConfig = new HelperConfig();
address vrfCoordinator = helperConfig.getConfig().vrfCoordinator;
address account = helperConfig.getConfig().account;
// create subscription
(uint256 subId, ) = createSubscription(vrfCoordinator, account);
return (subId, vrfCoordinator);
}
function createSubscription(address vrfCoordinator, address account) public returns (uint256, address) {
console.log("Creating subscription on chain Id: ", block.chainid);
vm.startBroadcast(account);
uint256 subId = VRFCoordinatorV2_5Mock(vrfCoordinator).createSubscription();
vm.stopBroadcast();
console.log("Your subscription Id is: ", subId);
console.log("Please update the subscription Id in your HelperConfig.s.sol");
return (subId, vrfCoordinator);
}
function run() public {
createSubscriptionUsingConfig();
}
}
contract FundSubscription is Script, CodeConstants {
uint256 public constant FUND_AMOUNT = 3 ether; // equals to 3 LINK
function fundSubscriptionUsingConfig() public {
HelperConfig helperConfig = new HelperConfig();
address vrfCoordinator = helperConfig.getConfig().vrfCoordinator;
uint256 subscriptionId = helperConfig.getConfig().subscriptionId;
address linkToken = helperConfig.getConfig().link;
address account = helperConfig.getConfig().account;
fundSubscription(vrfCoordinator, subscriptionId, linkToken, account);
}
function fundSubscription(address vrfCoordinator, uint256 subscriptionId, address linkToken, address account) public {
console.log("Funding subscription: ", subscriptionId);
console.log("Using vrfCoordinator: ", vrfCoordinator);
console.log("On ChainiId: ", block.chainid);
if (block.chainid == LOCAL_CHAIN_ID) {
vm.startBroadcast();
VRFCoordinatorV2_5Mock(vrfCoordinator).fundSubscription(subscriptionId, FUND_AMOUNT * 100);
vm.stopBroadcast();
} else {
vm.startBroadcast(account);
LinkToken(linkToken).transferAndCall(vrfCoordinator, FUND_AMOUNT, abi.encode(subscriptionId));
vm.stopBroadcast();
}
}
function run() public {
fundSubscriptionUsingConfig();
}
}
contract AddConsumer is Script {
function addConsumerUsingConfig(address mostRecentlyDeployed) public {
HelperConfig helperConfig = new HelperConfig();
uint256 subId = helperConfig.getConfig().subscriptionId;
address vrfCoordinator = helperConfig.getConfig().vrfCoordinator;
address account = helperConfig.getConfig().account;
addConsumer(mostRecentlyDeployed, vrfCoordinator, subId, account);
}
function addConsumer(address contractToAddToVrf, address vrfCoordinator, uint256 subId, address account) public {
console.log("Adding consumer contract: ", contractToAddToVrf);
console.log("To vrfCoordinator: ", vrfCoordinator);
console.log("On ChainId: ", block.chainid);
vm.startBroadcast(account);
VRFCoordinatorV2_5Mock(vrfCoordinator).addConsumer(subId, contractToAddToVrf);
vm.stopBroadcast();
}
function run() external {
address mostRecentlyDeployed = DevOpsTools.get_most_recent_deployment("Raffle", block.chainid);
addConsumerUsingConfig(mostRecentlyDeployed);
}
}

View File

@ -62,6 +62,7 @@ contract Raffle is VRFConsumerBaseV2Plus {
/* Events */ /* Events */
event RaffleEntered(address indexed player); event RaffleEntered(address indexed player);
event WinnerPicked(address indexed winner); event WinnerPicked(address indexed winner);
event RequestedRaffleWinner(uint256 indexed requestId);
constructor( constructor(
uint256 entranceFee, uint256 entranceFee,
@ -135,11 +136,15 @@ contract Raffle is VRFConsumerBaseV2Plus {
numWords: NUM_WORDS, numWords: NUM_WORDS,
extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false})) extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false}))
}); });
s_vrfCoordinator.requestRandomWords(request); uint256 requestId = s_vrfCoordinator.requestRandomWords(request);
// actually redundant because V2_5 has its own emit event with request id
// but its being done for the sake of ease development
emit RequestedRaffleWinner(requestId);
} }
// CEI: Check, Effects, Interactions Pattern // CEI: Check, Effects, Interactions Pattern
function fulfillRandomWords(uint256 /* requestId */, uint256[] calldata randomWords) internal virtual override { function fulfillRandomWords(uint256, /* requestId */ uint256[] calldata randomWords) internal virtual override {
// Checks: check values before executing function // Checks: check values before executing function
// Effect (Internal Contract State) // Effect (Internal Contract State)
@ -164,4 +169,20 @@ contract Raffle is VRFConsumerBaseV2Plus {
function getEntranceFee() external view returns (uint256) { function getEntranceFee() external view returns (uint256) {
return i_entranceFee; return i_entranceFee;
} }
function getRaffleState() external view returns (RaffleState) {
return s_raffleState;
}
function getPlayer(uint256 indexOfPlayer) external view returns (address) {
return s_players[indexOfPlayer];
}
function getLastTimeStamp() external view returns (uint256) {
return s_lastTimeStamp;
}
function getRecentWinner() external view returns (address) {
return s_recentWinner;
}
} }

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,249 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {Test} from "forge-std/Test.sol";
import {DeployRaffle} from "script/DeployRaffle.s.sol";
import {Raffle} from "src/Raffle.sol";
import {CodeConstants, HelperConfig} from "script/HelperConfig.s.sol";
import {Vm} from "forge-std/Vm.sol";
import {VRFCoordinatorV2_5Mock} from "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol";
contract RaffleTest is CodeConstants, Test {
Raffle public raffle;
HelperConfig public helperConfig;
uint256 entranceFee;
uint256 interval;
address vrfCoordinator;
bytes32 gasLane;
uint32 callbackGasLimit;
uint256 subscriptionId;
address public PLAYER = makeAddr("player");
uint256 public constant STARTING_PLAYER_BALANCE = 10 ether;
event RaffleEntered(address indexed player);
function setUp() external {
DeployRaffle deployer = new DeployRaffle();
(raffle, helperConfig) = deployer.deployContract();
HelperConfig.NetworkConfig memory config = helperConfig.getConfig();
entranceFee = config.entranceFee;
interval = config.interval;
vrfCoordinator = config.vrfCoordinator;
gasLane = config.gasLane;
callbackGasLimit = config.callbackGasLimit;
subscriptionId = config.subscriptionId;
vm.deal(PLAYER, STARTING_PLAYER_BALANCE);
}
function testRaffleInitializedInOpenState() public view {
assert(raffle.getRaffleState() == Raffle.RaffleState.OPEN);
}
function testRaffleRevertsWhenYouDontPayEnough() public {
// Arrange
vm.prank(PLAYER);
// Act / Asset
vm.expectRevert(Raffle.Raffle__SendMoreToEnterRaffle.selector);
// Assert
raffle.enterRaffle();
}
function testRaffleRecordsPlayersWhenTheyEnter() public {
// Arrange
vm.prank(PLAYER);
// Act
raffle.enterRaffle{value: entranceFee}();
// Assert
address playerRecorded = raffle.getPlayer(0);
assert(playerRecorded == PLAYER);
}
function testEnteringRaffleEmitsEvent() public {
// Arrange
vm.prank(PLAYER);
// Act
vm.expectEmit(true, false, false, false, address(raffle));
emit RaffleEntered(PLAYER);
// Assert
raffle.enterRaffle{value: entranceFee}();
}
function testDontAllowPlayersToEnterWhileRaffleIsCalculating() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: entranceFee}();
vm.warp(block.timestamp + interval + 1);
vm.roll(block.number + 1);
raffle.performUpkeep("");
// Act
vm.expectRevert(Raffle.Raffle__RaffleNotOpen.selector);
vm.prank(PLAYER);
raffle.enterRaffle{value: entranceFee}();
// Assert
}
// Check Upkeep
function testCheckUpkeepReturnsFalseIfItHasNoBalance() public {
// Arrange
vm.warp(block.timestamp + interval + 1);
vm.roll(block.number + 1);
// Act
(bool upkeepNeeded, ) = raffle.checkUpkeep("");
// Assert
assert(!upkeepNeeded);
}
function testCheckUpkeepReturnsFalseIfRaffleIsntOpen() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: entranceFee}();
vm.warp(block.timestamp + interval + 1);
vm.roll(block.number + 1);
raffle.performUpkeep("");
// Act
(bool upkeepNeeded, ) = raffle.checkUpkeep("");
// Assert
assert(!upkeepNeeded);
}
function testCheckUpkeepReturnsFalseIfEnoughTimeHasPassed() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: entranceFee}();
vm.warp(block.timestamp + interval + 1);
vm.roll(block.number + 1);
raffle.performUpkeep("");
vm.warp(block.timestamp + interval + 1);
vm.roll(block.number + 1);
// Act
(bool upkeepNeeded, ) = raffle.checkUpkeep("");
// Assert
assert(!upkeepNeeded);
}
function testCheckUpkeepReturnsTrueWhenParametersAreGood() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: entranceFee}();
vm.warp(block.timestamp + interval + 1);
vm.roll(block.number + 1);
// Act
(bool upkeepNeeded, ) = raffle.checkUpkeep("");
// Assert
assert(upkeepNeeded);
}
// Perform Upkeep
function testPerformUpkeepCanOnlyRunIfCheckUpkeepIsTrue() public {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: entranceFee}();
vm.warp(block.timestamp + interval + 1);
vm.roll(block.number + 1);
// Act / Assert
raffle.performUpkeep("");
}
function testPerformUpkeepRevertsIfCheckUpkeepIsFalse() public {
// Arrange
uint256 currentBalance = 0;
uint256 numPlayers = 0;
Raffle.RaffleState rState = raffle.getRaffleState();
vm.prank(PLAYER);
raffle.enterRaffle{value: entranceFee}();
currentBalance = currentBalance + entranceFee;
numPlayers = 1;
// Act / Assert
vm.expectRevert(
abi.encodeWithSelector(Raffle.Raffle__UpkeepNotNeeded.selector, currentBalance, numPlayers, rState)
);
raffle.performUpkeep("");
}
modifier raffleEntered() {
// Arrange
vm.prank(PLAYER);
raffle.enterRaffle{value: entranceFee}();
vm.warp(block.timestamp + interval + 1);
vm.roll(block.number + 1);
_;
}
function testPerformUpkeepUpdatesRaffleStateAndEmitsRequested() public raffleEntered {
// Act
vm.recordLogs();
raffle.performUpkeep("");
Vm.Log[] memory entries = vm.getRecordedLogs();
bytes32 requestId = entries[1].topics[1];
// Assert
Raffle.RaffleState raffleState = raffle.getRaffleState();
assert(uint256(requestId) > 0);
assert(uint256(raffleState) == 1);
}
// Fulfill Random Words
modifier skipFork() {
if (block.chainid != LOCAL_CHAIN_ID) {
return;
}
_;
}
function testFulfillRandomWordsCanOnlyBeCalledAfterPerformUpkeep(uint256 randomRequestId) public raffleEntered skipFork {
// Arrange / Assert
vm.expectRevert(VRFCoordinatorV2_5Mock.InvalidRequest.selector);
VRFCoordinatorV2_5Mock(vrfCoordinator).fulfillRandomWords(randomRequestId, address(raffle));
}
function testFulfillRandomWordsPicksAWinnerResetsAndSendsMoney() public raffleEntered skipFork {
// Arrange
uint256 additionalEntrants = 3;
uint256 startingIndex = 1;
address expectedWinner = address(1);
for(uint256 i = startingIndex; i < startingIndex + additionalEntrants; i++) {
address newPlayer = address(uint160(i)); // address(1)
hoax(newPlayer, 1 ether);
raffle.enterRaffle{value: entranceFee}();
}
uint256 startingTimeStamp = raffle.getLastTimeStamp();
uint256 winnerStartingBalance = expectedWinner.balance;
// Act
vm.recordLogs();
raffle.performUpkeep("");
Vm.Log[] memory entries = vm.getRecordedLogs();
bytes32 requestId = entries[1].topics[1];
VRFCoordinatorV2_5Mock(vrfCoordinator).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 = entranceFee * (additionalEntrants + 1);
assert(recentWinner == expectedWinner);
assert(uint256(raffleState) == 0);
assert(winnerBalance == winnerStartingBalance + prize);
assert(endingTimeStamp > startingTimeStamp);
}
}