Compare commits
5 Commits
b6fb56fd2c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cff92193ec | |||
| 4974b656b7 | |||
| 3679b8e055 | |||
| 7fc3d173d8 | |||
| 54dda02722 |
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -34,3 +34,12 @@
|
||||
[submodule "lottery/lib/chainlink-brownie-contracts"]
|
||||
path = lottery/lib/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
|
||||
[submodule "soldeer-package-manager/lib/forge-std"]
|
||||
path = soldeer-package-manager/lib/forge-std
|
||||
url = https://github.com/foundry-rs/forge-std
|
||||
|
||||
12
lottery/Makefile
Normal file
12
lottery/Makefile
Normal 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
|
||||
@@ -2,6 +2,16 @@
|
||||
src = "src"
|
||||
out = "out"
|
||||
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
|
||||
|
||||
Submodule lottery/lib/forge-std updated: b93cf4bc34...978ac6fadb
1
lottery/lib/foundry-devops
Submodule
1
lottery/lib/foundry-devops
Submodule
Submodule lottery/lib/foundry-devops added at df9f90b490
1
lottery/lib/solmate
Submodule
1
lottery/lib/solmate
Submodule
Submodule lottery/lib/solmate added at a9e3ea26a2
44
lottery/script/DeployRaffle.s.sol
Normal file
44
lottery/script/DeployRaffle.s.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
93
lottery/script/HelperConfig.s.sol
Normal file
93
lottery/script/HelperConfig.s.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
92
lottery/script/Interactions.s.sol
Normal file
92
lottery/script/Interactions.s.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ contract Raffle is VRFConsumerBaseV2Plus {
|
||||
/* Events */
|
||||
event RaffleEntered(address indexed player);
|
||||
event WinnerPicked(address indexed winner);
|
||||
event RequestedRaffleWinner(uint256 indexed requestId);
|
||||
|
||||
constructor(
|
||||
uint256 entranceFee,
|
||||
@@ -120,8 +121,8 @@ contract Raffle is VRFConsumerBaseV2Plus {
|
||||
return (upkeepNeeded, "");
|
||||
}
|
||||
|
||||
function performUpkeep(bytes calldata /* performData */) external {
|
||||
(bool upkeepNeeded, ) = checkUpkeep("");
|
||||
function performUpkeep(bytes calldata /* performData */ ) external {
|
||||
(bool upkeepNeeded,) = checkUpkeep("");
|
||||
if (!upkeepNeeded) {
|
||||
revert Raffle__UpkeepNotNeeded(address(this).balance, s_players.length, uint256(s_raffleState));
|
||||
}
|
||||
@@ -135,11 +136,15 @@ contract Raffle is VRFConsumerBaseV2Plus {
|
||||
numWords: NUM_WORDS,
|
||||
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
|
||||
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
|
||||
|
||||
// Effect (Internal Contract State)
|
||||
@@ -164,4 +169,20 @@ contract Raffle is VRFConsumerBaseV2Plus {
|
||||
function getEntranceFee() external view returns (uint256) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
55
lottery/test/mocks/LinkToken.sol
Normal file
55
lottery/test/mocks/LinkToken.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
249
lottery/test/unit/RaffleTest.t.sol
Normal file
249
lottery/test/unit/RaffleTest.t.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
43
soldeer-package-manager/.github/workflows/test.yml
vendored
Normal file
43
soldeer-package-manager/.github/workflows/test.yml
vendored
Normal 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
|
||||
18
soldeer-package-manager/.gitignore
vendored
Normal file
18
soldeer-package-manager/.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Compiler files
|
||||
cache/
|
||||
out/
|
||||
|
||||
# Ignores development broadcast logs
|
||||
!/broadcast
|
||||
/broadcast/*/31337/
|
||||
/broadcast/**/dry-run/
|
||||
|
||||
# Docs
|
||||
docs/
|
||||
|
||||
# Dotenv file
|
||||
.env
|
||||
|
||||
|
||||
# Soldeer
|
||||
/dependencies
|
||||
66
soldeer-package-manager/README.md
Normal file
66
soldeer-package-manager/README.md
Normal 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
|
||||
```
|
||||
19
soldeer-package-manager/foundry.toml
Normal file
19
soldeer-package-manager/foundry.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[profile.default]
|
||||
src = "src"
|
||||
out = "out"
|
||||
libs = ["lib", "dependencies"]
|
||||
remappings = [
|
||||
"@openzeppelin-contracts/=dependencies/@openzeppelin-contracts-5.0.2/src/",
|
||||
"forge-std/=dependencies/forge-std-1.9.5/src/",
|
||||
]
|
||||
|
||||
[soldeer]
|
||||
remappings_version = false
|
||||
remappings_location = "config"
|
||||
# recursive_deps = true
|
||||
|
||||
[dependencies]
|
||||
forge-std = "1.9.5"
|
||||
"@openzeppelin-contracts" = "5.0.2"
|
||||
|
||||
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
|
||||
19
soldeer-package-manager/script/Counter.s.sol
Normal file
19
soldeer-package-manager/script/Counter.s.sol
Normal file
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import {Script, console} from "forge-std/Script.sol";
|
||||
import {Counter} from "../src/Counter.sol";
|
||||
|
||||
contract CounterScript is Script {
|
||||
Counter public counter;
|
||||
|
||||
function setUp() public {}
|
||||
|
||||
function run() public {
|
||||
vm.startBroadcast();
|
||||
|
||||
counter = new Counter();
|
||||
|
||||
vm.stopBroadcast();
|
||||
}
|
||||
}
|
||||
13
soldeer-package-manager/soldeer.lock
Normal file
13
soldeer-package-manager/soldeer.lock
Normal file
@@ -0,0 +1,13 @@
|
||||
[[dependencies]]
|
||||
name = "@openzeppelin-contracts"
|
||||
version = "5.0.2"
|
||||
url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/5_0_2_14-03-2024_06:11:59_contracts.zip"
|
||||
checksum = "8bc4f0acc7c187771b878d46f7de4bfad1acad2eb5d096d9d05d34035853f5c3"
|
||||
integrity = "55881f6114aa36158566ef52b486d9d96b217c3e47d0e24bbd4994a8cfb5ee7c"
|
||||
|
||||
[[dependencies]]
|
||||
name = "forge-std"
|
||||
version = "1.9.5"
|
||||
url = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_9_5_21-12-2024_15:04:05_forge-std-1.9.zip"
|
||||
checksum = "57ada736f383289db77fac4472d48f820e7c98172cf9b01681b0c37065ce043f"
|
||||
integrity = "4753ffdfa0dde40878372b6a4d8e8fd1648b190b33996896c8b92f6f1680850f"
|
||||
14
soldeer-package-manager/src/Counter.sol
Normal file
14
soldeer-package-manager/src/Counter.sol
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
contract Counter {
|
||||
uint256 public number;
|
||||
|
||||
function setNumber(uint256 newNumber) public {
|
||||
number = newNumber;
|
||||
}
|
||||
|
||||
function increment() public {
|
||||
number++;
|
||||
}
|
||||
}
|
||||
24
soldeer-package-manager/test/Counter.t.sol
Normal file
24
soldeer-package-manager/test/Counter.t.sol
Normal file
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import {Test, console} from "forge-std/Test.sol";
|
||||
import {Counter} from "../src/Counter.sol";
|
||||
|
||||
contract CounterTest is Test {
|
||||
Counter public counter;
|
||||
|
||||
function setUp() public {
|
||||
counter = new Counter();
|
||||
counter.setNumber(0);
|
||||
}
|
||||
|
||||
function test_Increment() public {
|
||||
counter.increment();
|
||||
assertEq(counter.number(), 1);
|
||||
}
|
||||
|
||||
function testFuzz_SetNumber(uint256 x) public {
|
||||
counter.setNumber(x);
|
||||
assertEq(counter.number(), x);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user